/*
=== Scrolling Menu API ESP32 ChatGPT version ===
ESP32 Chuck Norris Jokes - integrated with U8g2 menu
- Press Enter on "Chuck Jokes" to go to screen 1 (joke view).
- Joke will scroll (if wide) or be centered, then after a short delay a new joke is fetched.
- Enter again to advance to screen 2 (or back to menu).
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "U8g2lib.h"
// WiFi (Wokwi default)
const char* ssid = "Wokwi-GUEST";
const char* password = "";
// Constructor for U8g2lib
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0);
// ------------------ Start generated bitmaps from image2cpp ---------------------------------
// 'Sunrise_Sunset_Icon', 16x16px
const unsigned char Sunrise_Sunset_Icon[] PROGMEM = { // 'PROGMEM" places this variable in Flash (32k) memory as opposed to SRAM (2k)
0x88, 0x00, 0x72, 0x02, 0x8c, 0x01, 0x05, 0x05, 0x02, 0x02, 0x02, 0x02, 0x02, 0x42, 0x05, 0x45,
0x8c, 0xc1, 0x72, 0xc2, 0x88, 0xe0, 0x00, 0xf0, 0x00, 0x78, 0x00, 0x7c, 0xc0, 0x3f, 0x00, 0x0f
};
// 'Dad_Jokes_Icon', 16x16px
const unsigned char Dad_Jokes_Icon[] PROGMEM = {
0xc0, 0x03, 0x30, 0x0c, 0x0c, 0x30, 0x04, 0x20, 0x72, 0x4e, 0x22, 0x44, 0x01, 0x80, 0x01, 0x80,
0x01, 0x80, 0x19, 0x98, 0xf2, 0x4f, 0xe2, 0x47, 0x84, 0x21, 0x0c, 0x30, 0x30, 0x0c, 0xc0, 0x03
};
// 'Useless_Facts_Icon', 16x16px
const unsigned char Useless_Facts_Icon[] PROGMEM = {
0x00, 0x00, 0x00, 0x40, 0x00, 0x60, 0x00, 0x30, 0x00, 0x18, 0x00, 0x0c, 0x00, 0x0e, 0x00, 0x07,
0x80, 0x03, 0x87, 0x03, 0xcf, 0x01, 0xde, 0x01, 0xfc, 0x00, 0xf8, 0x00, 0x70, 0x00, 0x20, 0x00
};
// 'Quote_Icon', 16x16px
const unsigned char Quote_Icon[] PROGMEM = {
0xcf, 0x03, 0xcf, 0x03, 0xcf, 0x03, 0xc3, 0x00, 0xc3, 0x00, 0x86, 0x01, 0x0c, 0x03, 0x00, 0x00,
0x00, 0x00, 0xc0, 0xf3, 0xc0, 0xf3, 0xc0, 0xf3, 0x00, 0xc3, 0x00, 0xc3, 0x80, 0x61, 0xc0, 0x30
};
// 'Chuck_Icon_new', 16x16px
const unsigned char Chuck_Icon[] PROGMEM = {
0x00, 0x00, 0x42, 0x42, 0xa2, 0x45, 0x16, 0x68, 0xfc, 0x3f, 0x08, 0x10, 0x68, 0x16, 0x08, 0x10,
0x08, 0x11, 0x88, 0x11, 0x08, 0x10, 0xc8, 0x13, 0x28, 0x14, 0x08, 0x10, 0x50, 0x0a, 0xa0, 0x05
};
// Array of all icon bitmaps.
const unsigned char* menu_icon[5] = {
Sunrise_Sunset_Icon,
Dad_Jokes_Icon,
Useless_Facts_Icon,
Quote_Icon,
Chuck_Icon
};
// 'Boarder', 128x20px
const unsigned char Boarder[] PROGMEM = {
0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x02, 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, 0x04,
0x02, 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, 0x04,
0x02, 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, 0x04,
0x02, 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, 0x04,
0x02, 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, 0x04,
0x02, 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, 0x04,
0x02, 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, 0x04,
0x02, 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, 0x04,
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01
};
// 'Scroll Bar', 8x64px
const unsigned char Scroll_Bar[] 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, 0x40
};
// ------------------ End generated bitmaps from image2cpp ---------------------------------
// 2D array of strings for menu items
const int NUM_ITEMS = 5;
char menu_item_local[NUM_ITEMS][20] = {
{"Sunrise Sunset"},
{"Dad Jokes"},
{"Useless Facts"},
{"Quotes"},
{"Chuck Jokes"}
};
// ===== Start OLED menu variables =====
int item_sel_previous; // menu item before selected
int item_selected = 0; // selected menu item
int item_sel_next; // menu item after selected
#define BUTTON_UP_PIN 27 // pin for UP button
#define BUTTON_DOWN_PIN 26 // pin for DOWN button
#define BUTTON_SELECT_PIN 25 // pin for SELECT button
int button_up_clicked = 0; // only perform action when button is clicked, and wait until another press
int button_select_clicked = 0; // same as above
int button_down_clicked = 0; // same as above
int current_screen = 0; // 0 = menu, 1 = API string, 2 = unused
// ===== End OLED menu variables =====
// ===== Scrolling / API variables =====
u8g2_uint_t offset = 0; // current offset for the scrolling text
u8g2_uint_t width = 0; // pixel width of the scrolling text (must be lesser than 128 unless U8G2_16BIT is defined
String jokeText = "";
bool jokeAvailable = false;
bool needFetch = false;
bool waitingForNext = false;
unsigned long waitStart = 0;
unsigned long lastScrollMillis = 0;
const unsigned long SCROLL_STEP_MS = 1; // ms per pixel step (smaller → faster)
const unsigned long BETWEEN_JOKES_MS = 2000; // wait between jokes when one finishes (ms)
const int CHUCK_INDEX = 4; // index of "Chuck Jokes" in your menu (0-based)
u8g2_uint_t displayWidth = 128;
// For detecting screen entry
int prev_screen = -1;
// ===== helper: fetch one Chuck joke =====
bool fetchChuckJoke() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected - cannot fetch joke");
return false;
}
HTTPClient http;
const char* url = "https://api.chucknorris.io/jokes/random";
http.begin(url); // for HTTPS this can work in Wokwi; on real hardware you may need WiFiClientSecure
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
String payload = http.getString();
// parse into a local StaticJsonDocument to avoid reusing a global improperly
StaticJsonDocument<1024> doc;
DeserializationError err = deserializeJson(doc, payload);
if (err) {
Serial.print(F("deserializeJson() failed: "));
Serial.println(err.c_str());
http.end();
return false;
}
const char* val = doc["value"];
jokeText = String(val ? val : ""); // copy to String
// prepare marquee metrics
u8g2.setFont(u8g2_font_10x20_mr);
width = u8g2.getUTF8Width(jokeText.c_str());
offset = displayWidth; // start off-right so it scrolls in
lastScrollMillis = millis();
jokeAvailable = true;
waitingForNext = false;
Serial.println("Fetched joke:");
Serial.println(jokeText);
http.end();
return true;
} else {
Serial.print("HTTP GET failed, code=");
Serial.println(httpCode);
}
http.end();
return false;
}
// ------------------- Setup -----------------------
void setup() {
Serial.begin(115200);
// WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(250);
Serial.print(".");
}
Serial.println();
Serial.print("WiFi connected: ");
Serial.println(WiFi.localIP());
// U8g2
u8g2.begin();
u8g2.setBitmapMode(1);
u8g2.setColorIndex(1);
u8g2.setFont(u8g2_font_10x20_mr);
displayWidth = u8g2.getDisplayWidth();
// Buttons
pinMode(BUTTON_UP_PIN, INPUT_PULLUP);
pinMode(BUTTON_DOWN_PIN, INPUT_PULLUP);
pinMode(BUTTON_SELECT_PIN, INPUT_PULLUP);
}
void loop() {
u8g2_uint_t x;
// --- handle Up/Down navigation on menu (only active in screen 0) ---
if (current_screen == 0) {
if ((digitalRead(BUTTON_UP_PIN) == LOW) && (button_up_clicked == 0)) {
item_selected--;
button_up_clicked = 1;
if (item_selected < 0) item_selected = NUM_ITEMS - 1;
}
if ((digitalRead(BUTTON_DOWN_PIN) == LOW) && (button_down_clicked == 0)) {
item_selected++;
button_down_clicked = 1;
if (item_selected >= NUM_ITEMS) item_selected = 0;
}
if ((digitalRead(BUTTON_UP_PIN) == HIGH) && (button_up_clicked == 1)) {
button_up_clicked = 0;
}
if ((digitalRead(BUTTON_DOWN_PIN) == HIGH) && (button_down_clicked == 1)) {
button_down_clicked = 0;
}
}
// --- handle Enter / Select (cycles screens) ---
if ((digitalRead(BUTTON_SELECT_PIN) == LOW) && (button_select_clicked == 0)) {
button_select_clicked = 1;
if (current_screen == 0) current_screen = 1;
else if (current_screen == 1) current_screen = 2;
else current_screen = 0;
}
if ((digitalRead(BUTTON_SELECT_PIN) == HIGH) && (button_select_clicked == 1)) {
button_select_clicked = 0;
}
// --- update previous/next indices (circular) ---
item_sel_previous = item_selected - 1;
if (item_sel_previous < 0) item_sel_previous = NUM_ITEMS - 1;
item_sel_next = item_selected + 1;
if (item_sel_next >= NUM_ITEMS) item_sel_next = 0;
// --- detect screen entry: when we *enter* screen 1 for Chuck, mark needFetch ---
if (prev_screen != current_screen) {
if (current_screen == 1 && item_selected == CHUCK_INDEX) {
needFetch = true; // next loop we'll fetch a joke (or immediately below)
jokeAvailable = false;
waitingForNext = false;
} else {
// left the Chuck screen or entered screen 1 for some other menu item
jokeAvailable = false;
needFetch = false;
waitingForNext = false;
}
prev_screen = current_screen;
}
// If we are on Chuck Jokes screen and we need to fetch, do it now
if (current_screen == 1 && item_selected == CHUCK_INDEX && needFetch) {
// Attempt fetch; if it fails we'll try again next loop (could add retry/backoff)
if (fetchChuckJoke()) {
needFetch = false;
} else {
// keep needFetch true so we'll retry; add a short delay so we don't hammer
delay(200);
}
}
// ----------------- Drawing with single firstPage/nextPage -----------------
u8g2.firstPage();
do {
if (current_screen == 0) { // MENU SCREEN
// draw the border and three items (same as your original)
u8g2.drawXBMP(0, 22, 128, 20, Boarder);
u8g2.setFont(u8g2_font_7x14_tf);
u8g2.drawStr(26, 15, menu_item_local[item_sel_previous]);
u8g2.drawXBMP(4, 2, 16, 16, menu_icon[item_sel_previous]);
u8g2.setFont(u8g2_font_7x14B_tf);
u8g2.drawStr(25, 37, menu_item_local[item_selected]);
u8g2.drawXBMP(4, 24, 16, 16, menu_icon[item_selected]);
u8g2.setFont(u8g2_font_7x14_tf);
u8g2.drawStr(26, 59, menu_item_local[item_sel_next]);
u8g2.drawXBMP(4, 46, 16, 16, menu_icon[item_sel_next]);
u8g2.drawXBMP(120, 0, 8, 64, Scroll_Bar);
u8g2.drawBox(125, (64 / NUM_ITEMS) * item_selected, 3, (64 / NUM_ITEMS));
}
else if (current_screen == 1) { // JOKE / sub-screen
if (item_selected == CHUCK_INDEX) {
// Chuck jokes view
u8g2.setFont(u8g2_font_10x20_mr);
if (!jokeAvailable) {
// show "Loading..." while fetching
u8g2.setCursor(0, 30);
if (needFetch) u8g2.print("Loading...");
else u8g2.print("Press Enter");
} else {
// we have a joke, draw it (centered if fits; scroll if wider)
if (width <= displayWidth - 2) {
int xpos = (displayWidth - width) / 2;
u8g2.drawUTF8(xpos, 30, jokeText.c_str());
} else {
// marquee: draw repeated copies so it wraps nicely
u8g2_uint_t xx = offset;
do {
u8g2.drawUTF8(xx, 30, jokeText.c_str());
xx += width;
} while (xx < displayWidth);
}
}
} else {
// placeholder for other API items until you add their endpoints
u8g2.setFont(u8g2_font_7x14_tf);
u8g2.setCursor(0, 30);
u8g2.print("No API hooked up");
}
}
else if (current_screen == 2) {
// placeholder for screen 2
u8g2.setFont(u8g2_font_7x14_tf);
u8g2.drawStr(0, 30, "Screen 2 (placeholder)");
}
} while (u8g2.nextPage());
// ----------------- After drawing: update marquee timers / state machine -----------------
if (current_screen == 1 && item_selected == CHUCK_INDEX && jokeAvailable) {
unsigned long now = millis();
if (width <= displayWidth - 2) {
// doesn't need to scroll; just wait then fetch next joke
if (!waitingForNext) {
waitingForNext = true;
waitStart = now;
} else {
if (now - waitStart >= BETWEEN_JOKES_MS) {
needFetch = true;
jokeAvailable = false; // will fetch new on next loop
waitingForNext = false;
}
}
} else {
// needs scrolling
if (now - lastScrollMillis >= SCROLL_STEP_MS) {
offset -= 1;
lastScrollMillis = now;
}
// when fully scrolled past left edge we start wait timer and then fetch new joke
if ((signed)offset < -((signed)width)) {
if (!waitingForNext) {
waitingForNext = true;
waitStart = now;
} else {
if (now - waitStart >= BETWEEN_JOKES_MS) {
needFetch = true;
jokeAvailable = false;
waitingForNext = false;
}
}
}
}
}
// small yield to keep UI responsive
delay(10);
}