/******************************************************************************
* MUI & Rotary Encoder Demo for ESP32-S3
*
* This sketch demonstrates:
* - Rotary encoder navigation for a MUI-based menu
* - Splash screens at startup (Loading, Demo, T.I. Inc)
* - A 5-form menu system (MAIN_MENU, SET_DURATION, SET_SPEED, CALIBRATE, ERROR_INFO)
* - Numeric input for duration, speed, and calibration
* - Error handling (calibration > 4.0 => ERROR_INFO)
*
* Pin assignments:
* SDA_PIN = 8, SCL_PIN = 9 (I2C lines for OLED)
* ENC_SW = 5 => SELECT
* ENC_CLK = 7 => NEXT
* ENC_DT = 6 => PREV
*
* Author: J. Walker: Refractored from original T.I display code - see github https://github.com/WeffreyJ/T.I-Inc)
* Date: 2025-08-03
* Target: ESP32-S3
******************************************************************************/
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include <MUIU8g2.h> // MUI for U8g2 library header
#ifdef U8X8_HAVE_HW_I2C
#include <SPI.h>
#endif
// ------------------- PIN ASSIGNMENTS -------------------
#define SDA_PIN 8
#define SCL_PIN 9
#define ENC_SW 5 // Rotary encoder push button => "SELECT"
#define ENC_CLK 7 // Rotary encoder phase A => "NEXT"
#define ENC_DT 6 // Rotary encoder phase B => "PREV"
// ------------------- CONSTANTS & DEFINES -------------------
#define DEBOUNCE_DELAY 5 // ms: Debounce delay for rotary ISR
#define SPLASH_SCREEN_DURATION 3000 // ms: Duration for each splash screen
// MUI callback message types (if not included by MUI library)
#define MUIF_MSG_DRAW 0
#define MUIF_MSG_FORM 1
#define MUIF_MSG_SELECT 2
// ------------------- FORWARD DECLARATIONS -------------------
void displayLoadingScreen();
void displayMenuDemoScreen();
void displayTIIncScreen();
void IRAM_ATTR handleEncoderInterrupt();
// MUI callbacks
uint8_t mui_hrule(mui_t *ui, uint8_t msg);
uint8_t mui_cb_setDuration(mui_t *ui, uint8_t msg);
uint8_t mui_cb_setSpeed(mui_t *ui, uint8_t msg);
uint8_t mui_cb_setCalib(mui_t *ui, uint8_t msg);
uint8_t mui_cb_finishCalib(mui_t *ui, uint8_t msg);
uint8_t mui_cb_finishDuration(mui_t *ui, uint8_t msg);
uint8_t mui_cb_finishSpeed(mui_t *ui, uint8_t msg);
uint8_t mui_cb_errorReset(mui_t *ui, uint8_t msg);
// ------------------- GLOBAL OBJECTS -------------------
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
MUIU8G2 mui;
// ------------------- MENU & ENCODER VARIABLES -------------------
// Final user-selected values
int durationVal = 1000; // [100..2000] ms
float speedVal = 0.5; // [0.5..4.0]
float calibrationVal = 0.0; // [0.0..4.0], error if >4.0
// Indexes for MUI numeric input
uint8_t durationIndex = 10; // [1..20], => 100..2000 ms
uint8_t speedIndex = 5; // [5..40], => 0.5..4.0
uint8_t calibIndex = 0; // [0..40], => 0.0..4.0
static uint8_t speed_value = (uint8_t) speedVal;
static uint8_t duration_value = (uint8_t) durationVal;
// Rotary encoder flags
volatile bool encoderFlag = false;
volatile int8_t encoderDirection = 0; // +1=clockwise, -1=counterclockwise
// Screen redraw management
bool needsRedraw = true; // Forces refresh when user interacts
// ---------------------------------------------------------------------------
// MUI Field Definitions & Form Strings
// ---------------------------------------------------------------------------
uint8_t mui_speed_select(mui_t *ui, uint8_t msg) {
if (msg == MUIF_MSG_DRAW) {
char buf[10];
sprintf(buf, "Speed: %d", speedVal);
}
if (msg == MUIF_MSG_EVENT_NEXT) {speedVal++; }
if (msg == MUIF_MSG_EVENT_PREV) {speedVal--; }
return 0;
}
uint8_t mui_duration_select(mui_t *ui, uint8_t msg) {
if (msg == MUIF_MSG_DRAW) {
char buf[10];
sprintf(buf, "Duration: %ds", durationVal);
}
if (msg == MUIF_MSG_EVENT_NEXT) { durationVal++; }
if (msg == MUIF_MSG_EVENT_PREV) { durationVal--; }
return 0;
}
muif_t muif_list[] = {
MUIF_BUTTON("BN", mui_u8g2_btn_goto_wm_fi),
MUIF_U8G2_U8_MIN_MAX("BX", &speed_value, 0, 255, mui_speed_select),
MUIF_U8G2_U8_MIN_MAX("BY", &duration_value, 0, 60, mui_duration_select),
};
fds_t fds_data[] =
// --- MAIN MENU ---
MUI_FORM(1)
MUI_STYLE(1)
MUI_LABEL(32, 8, "MAIN MENU") // Centered title
MUI_XY("HR", 0, 13) // Horizontal line under title
MUI_STYLE(0)
MUI_XYAT("BN", 35, 25, 2, "Set Speed")
MUI_XYAT("BN", 42, 40, 3, "Set Duration")
MUI_XYAT("BN", 35, 55, 4, "Calibrate")
// --- SET SPEED PAGE ---
MUI_FORM(2)
MUI_STYLE(1)
MUI_LABEL(30, 8, "SET SPEED") // Centered title
MUI_XY("HR", 0, 13)
MUI_STYLE(0)
MUI_XYT("BX", 20, 30, "Speed:") // Speed label
MUI_XYAT("BN", 20, 50, 1, "Back") // Return to Main Menu
MUI_XYAT("BN", 80, 50, 5, "Select") // Confirm selection
// --- SET DURATION PAGE ---
MUI_FORM(3)
MUI_STYLE(1)
MUI_LABEL(25, 8, "SET DURATION")
MUI_XY("HR", 0, 13)
MUI_STYLE(0)
MUI_XYT("BX", 20, 30, "Duration:")
MUI_XYAT("BN", 20, 50, 1, "Back")
MUI_XYAT("BN", 80, 50, 6, "Select")
// --- CALIBRATE PAGE ---
MUI_FORM(4)
MUI_STYLE(1)
MUI_LABEL(30, 8, "CALIBRATE")
MUI_XY("HR", 0, 13)
MUI_STYLE(0)
MUI_XYAT("BN", 20, 25, 7, "Easy")
MUI_XYAT("BN", 20, 40, 8, "Medium")
MUI_XYAT("BN", 20, 55, 9, "Custom")
MUI_XYAT("BN", 20, 65, 1, "Back")
;
// ---------------------------------------------------------------------------
// Splash Screens
// ---------------------------------------------------------------------------
void displayLoadingScreen() {
Serial.println("Displaying Loading Screen...");
const int animationCycles = 10;
for (int i = 0; i < animationCycles; i++) {
u8g2.firstPage();
do {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(10, 30, "Loading");
// Animate up to 3 dots
int numDots = i % 4;
for (int j = 0; j < numDots; j++) {
u8g2.drawStr(70 + (j * 5), 30, ".");
}
} while (u8g2.nextPage());
delay(300);
}
Serial.println("Loading Screen Finished.");
}
void displayMenuDemoScreen() {
Serial.println("Displaying Menu Demo Screen...");
unsigned long startTime = millis();
while (millis() - startTime < SPLASH_SCREEN_DURATION) {
u8g2.firstPage();
do {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(30, 25, "Menu Demo");
u8g2.drawStr(45, 45, "V4.1.2");
} while (u8g2.nextPage());
}
Serial.println("Menu Demo Screen Finished.");
}
void displayTIIncScreen() {
Serial.println("Displaying T.I. Inc Screen...");
unsigned long startTime = millis();
while (millis() - startTime < SPLASH_SCREEN_DURATION) {
u8g2.firstPage();
do {
u8g2.clearBuffer();
u8g2.setFontMode(1);
u8g2.setBitmapMode(1);
// Simple ellipse with text
u8g2.drawEllipse(64, 32, 58, 24);
u8g2.setFont(u8g2_font_t0_14b_tr);
u8g2.drawStr(35, 35, "T.I. Inc");
} while (u8g2.nextPage());
}
// Restore
u8g2.setFontMode(0);
u8g2.setBitmapMode(0);
Serial.println("T.I. Inc Screen Finished.");
}
// ---------------------------------------------------------------------------
// Rotary Encoder ISR
// ---------------------------------------------------------------------------
void IRAM_ATTR handleEncoderInterrupt() {
static unsigned long lastInterruptTime = 0;
unsigned long interruptTime = millis();
// Debounce
if (interruptTime - lastInterruptTime < DEBOUNCE_DELAY) {
return;
}
lastInterruptTime = interruptTime;
bool clkState = digitalRead(ENC_CLK);
bool dtState = digitalRead(ENC_DT);
if (clkState != dtState) {
encoderDirection = 1; // Clockwise
} else {
encoderDirection = -1; // Counterclockwise
}
encoderFlag = true;
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
void setup() {
Serial.begin(115200);
Serial.println("--- MUI Rotary Encoder Demo ---");
// 1) Initialize I2C
Wire.begin(SDA_PIN, SCL_PIN);
// 2) Begin U8g2 with MUI pin mapping:
// ENC_SW => select
// ENC_CLK => next
// ENC_DT => prev
u8g2.begin(ENC_SW, ENC_CLK, ENC_DT,
U8X8_PIN_NONE, U8X8_PIN_NONE, U8X8_PIN_NONE);
// 3) Font mode => typical baseline rendering
u8g2.setFontMode(1);
u8g2.setFont(u8g2_font_helvR08_tr); // Set a readable font
// 4) Initialize MUI
mui.begin(u8g2, fds_data, muif_list,
sizeof(muif_list) / sizeof(muif_t));
// 5) Splash screens (Optional)
displayLoadingScreen();
displayMenuDemoScreen();
displayTIIncScreen();
// 6) Configure rotary pins & interrupt
pinMode(ENC_CLK, INPUT_PULLUP);
pinMode(ENC_DT, INPUT_PULLUP);
pinMode(ENC_SW, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENC_CLK), handleEncoderInterrupt, CHANGE);
// 7) Enter Main Menu (Form 1, since it's the first menu)
mui.gotoForm(1, 0); // 👈 Changed from `0` to `1`
// 8) Force initial screen update
needsRedraw = true;
}
// ---------------------------------------------------------------------------
// Main Loop
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Main Loop
// ---------------------------------------------------------------------------
void loop() {
// 1) Process rotary ISR events
if (encoderFlag) {
encoderFlag = false;
Serial.print("Encoder Direction: ");
Serial.println(encoderDirection);
if (encoderDirection == 1) {
mui.nextField(); // Move to next menu item
} else if (encoderDirection == -1) {
mui.prevField(); // Move to previous menu item
}
needsRedraw = true; // Trigger screen redraw
}
// 2) Check MUI activity
if (mui.isFormActive()) {
// Poll standard menu events from U8G2 (select button, etc.)
int menuEvent = u8g2.getMenuEvent();
if (menuEvent != 0) { // 0 => no event
Serial.print("Menu Event: ");
Serial.println(menuEvent);
switch (menuEvent) {
case U8X8_MSG_GPIO_MENU_SELECT:
mui.sendSelect(); // Trigger action
needsRedraw = true;
break;
case U8X8_MSG_GPIO_MENU_NEXT:
case U8X8_MSG_GPIO_MENU_PREV:
// Handled by rotary encoder interrupt logic
break;
}
}
// 3) Redraw UI if needed
if (needsRedraw) {
u8g2.firstPage();
do {
mui.draw(); // Draw MUI elements
} while (u8g2.nextPage());
needsRedraw = false; // Reset flag after drawing
}
} else {
// 4) If no menu is active, optionally clear the display
u8g2.clearDisplay();
}
}