#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ezButton.h>
// --- Pin Definitions ---
#define ENCODER_PIN_A 2
#define ENCODER_PIN_B 3
#define ENCODER_BTN_PIN 4
// --- OLED Display Configuration ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// --- Timing Configuration ---
const unsigned long BLINK_INTERVAL = 500; // Blinking interval in ms
const unsigned long DEBOUNCE_TIME = 50; // Button debounce time in ms
const unsigned long RETURN_DELAY = 300; // Delay after returning to menu in ms
// --- Menu Configuration ---
const uint8_t MENU_ITEM_COUNT = 3; // W, V, A
const int MENU_CIRCLE_X_OFFSET = -28;
const int MENU_CIRCLE_RADIUS = 30;
const int MENU_BOX_WIDTH = 24;
const int MENU_BOX_HEIGHT = 20;
const int MENU_BOX_X_OFFSET = -5;
const int MENU_TEXT_X_OFFSET = 7;
const int MENU_TEXT_Y_OFFSET = 2;
const int MENU_BOX_Y_TOP = 2;
// --- Graph Configuration ---
#define GRAPH_WIDTH (SCREEN_WIDTH / 2) // Common width for all graphs
#define GRAPH_HEIGHT SCREEN_HEIGHT // Common height for all graphs
#define GRAPH_POINTS 64 // Number of data points for graphs
// --- Back Arrow Configuration ---
const int BACK_ARROW_BOX_WIDTH = 18;
const int BACK_ARROW_BOX_HEIGHT = 18;
const int BACK_ARROW_BOX_X_OFFSET = -2;
const int BACK_ARROW_BOX_Y = 2;
const int BACK_ARROW_TEXT_X_OFFSET = 3;
const int BACK_ARROW_TEXT_Y_OFFSET = 2;
// --- Info Panel Configuration ---
const int INFO_PANEL_X_OFFSET = 4;
const int INFO_PANEL_Y = 24;
const int INFO_PANEL_LINE1_Y = 4;
const int INFO_PANEL_LINE2_Y = 16;
const int INFO_PANEL_VALUE_Y_OFFSET = 6;
const int INFO_PANEL_VALUE_LINE_SPACING = 12;
// --- Screen States ---
enum ScreenState {
SCREEN_MENU,
SCREEN_W_GRAPH,
SCREEN_V_GRAPH,
SCREEN_A_GRAPH
};
ScreenState currentScreen = SCREEN_MENU;
// --- Global Variables ---
volatile long encoderPosition = 0;
volatile uint8_t lastEncoded = 0; // Use uint8_t as it only needs 2 bits
uint8_t menuIndex = 0; // 0:W, 1:V, 2:A
ezButton encoderBtn(ENCODER_BTN_PIN);
// Graph data arrays and indices
float wValues[GRAPH_POINTS] = {0};
uint8_t wIndex = 0;
float vValues[GRAPH_POINTS] = {0};
uint8_t vIndex = 0;
float aValues[GRAPH_POINTS] = {0};
uint8_t aIndex = 0;
// State management variables
bool displayNeedsUpdate = true; // Flag to control display updates
bool blinkState = false;
unsigned long lastBlinkTime = 0;
bool justEnteredScreen = false; // To prevent immediate exit from graph screens
unsigned long screenTransitionTime = 0; // For non-blocking return delay
// --- Function Prototypes ---
void handleEncoder();
void updateMenuScreen();
void updateWGraphScreen();
void updateVGraphScreen();
void updateAGraphScreen();
void drawMenu();
void drawWGraphScreen();
void drawVGraphScreen();
void drawAGraphScreen();
void drawGraphAxes(uint8_t graphWidth, uint8_t graphHeight);
void drawGraphData(float values[], uint8_t index, uint8_t points, uint8_t graphWidth, uint8_t graphHeight);
void drawBackArrowBox(bool blink);
void drawInfoPanel(float w, float v, float a, const char* formulaLine1, const char* formulaLine2, uint8_t primaryValueIndex);
// --- Interrupt Service Routine ---
void handleEncoder() {
uint8_t MSB = digitalRead(ENCODER_PIN_A);
uint8_t LSB = digitalRead(ENCODER_PIN_B);
uint8_t encoded = (MSB << 1) | LSB;
uint8_t sum = (lastEncoded << 2) | encoded;
// Determine direction: CLOCKWISE
if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) {
encoderPosition++;
}
// Determine direction: COUNTER-CLOCKWISE
if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) {
encoderPosition--;
}
lastEncoded = encoded; // Store this state
}
// --- Setup Function ---
void setup() {
pinMode(ENCODER_PIN_A, INPUT_PULLUP);
pinMode(ENCODER_PIN_B, INPUT_PULLUP);
// Read initial state for encoder
uint8_t MSB = digitalRead(ENCODER_PIN_A);
uint8_t LSB = digitalRead(ENCODER_PIN_B);
lastEncoded = (MSB << 1) | LSB;
// Attach interrupts
attachInterrupt(digitalPinToInterrupt(ENCODER_PIN_A), handleEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_PIN_B), handleEncoder, CHANGE);
// Initialize display
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Don't proceed, loop forever
}
display.clearDisplay();
display.display(); // Show initial clear screen
// Initialize button
encoderBtn.setDebounceTime(DEBOUNCE_TIME);
// Initial draw
drawMenu();
displayNeedsUpdate = true; // Ensure the first draw happens in loop
}
// --- Main Loop ---
void loop() {
unsigned long currentTime = millis();
encoderBtn.loop(); // Update button state
// --- Blinking Logic ---
if (currentTime - lastBlinkTime >= BLINK_INTERVAL) {
blinkState = !blinkState;
lastBlinkTime = currentTime;
displayNeedsUpdate = true; // Need to redraw blinking elements
}
// --- State Machine ---
switch (currentScreen) {
case SCREEN_MENU:
updateMenuScreen(currentTime);
break;
case SCREEN_W_GRAPH:
updateWGraphScreen(currentTime);
break;
case SCREEN_V_GRAPH:
updateVGraphScreen(currentTime);
break;
case SCREEN_A_GRAPH:
updateAGraphScreen(currentTime);
break;
}
// --- Display Update ---
// Only call display.display() if something changed
if (displayNeedsUpdate) {
// Clear display before redrawing based on current state
display.clearDisplay();
switch (currentScreen) {
case SCREEN_MENU:
drawMenu();
break;
case SCREEN_W_GRAPH:
drawWGraphScreen();
break;
case SCREEN_V_GRAPH:
drawVGraphScreen();
break;
case SCREEN_A_GRAPH:
drawAGraphScreen();
break;
}
display.display();
displayNeedsUpdate = false; // Reset flag after update
}
}
// --- Screen Update Functions ---
void updateMenuScreen(unsigned long currentTime) {
static long lastEncoderPos = 0;
// Prevent interaction shortly after returning from a graph screen
if (currentTime - screenTransitionTime < RETURN_DELAY) {
return;
}
// Handle encoder rotation for menu navigation
// Divide by 4 for debouncing/filtering encoder steps
long currentEncoderPos = encoderPosition / 4;
if (currentEncoderPos != lastEncoderPos) {
if (currentEncoderPos > lastEncoderPos) {
menuIndex = (menuIndex + 1) % MENU_ITEM_COUNT;
} else {
// Modulo arithmetic for wrapping around backwards
menuIndex = (menuIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
}
lastEncoderPos = currentEncoderPos;
displayNeedsUpdate = true; // Menu selection changed
}
// Handle button press to enter graph screens
if (encoderBtn.isPressed()) {
justEnteredScreen = true; // Set flag for the new screen
screenTransitionTime = currentTime; // Record time for potential immediate exit check
switch (menuIndex) {
case 0: currentScreen = SCREEN_W_GRAPH; break;
case 1: currentScreen = SCREEN_V_GRAPH; break;
case 2: currentScreen = SCREEN_A_GRAPH; break;
}
displayNeedsUpdate = true; // Screen changed
}
}
void updateWGraphScreen(unsigned long currentTime) {
// Sample new data (replace with actual sensor reading)
float w = (sin(currentTime / 1000.0) + 1.0) / 2.0; // Example: 0 to 1 sine wave
wValues[wIndex] = w;
wIndex = (wIndex + 1) % GRAPH_POINTS;
displayNeedsUpdate = true; // Data changed
// Handle exit logic
if (justEnteredScreen) {
// Wait for button release after entering
if (encoderBtn.isReleased()) {
justEnteredScreen = false;
}
} else {
// If button is pressed and released (after initial entry), return to menu
if (encoderBtn.isReleased()) {
currentScreen = SCREEN_MENU;
screenTransitionTime = currentTime; // Record time for return delay
displayNeedsUpdate = true; // Screen changed
}
}
}
void updateVGraphScreen(unsigned long currentTime) {
// Sample new data (replace with actual sensor reading)
float v = (cos(currentTime / 1200.0) + 1.0) / 2.0; // Example: 0 to 1 cosine wave
vValues[vIndex] = v;
vIndex = (vIndex + 1) % GRAPH_POINTS;
displayNeedsUpdate = true; // Data changed
// Handle exit logic (same as W graph)
if (justEnteredScreen) {
if (encoderBtn.isReleased()) {
justEnteredScreen = false;
}
} else {
if (encoderBtn.isReleased()) {
currentScreen = SCREEN_MENU;
screenTransitionTime = currentTime;
displayNeedsUpdate = true;
}
}
}
void updateAGraphScreen(unsigned long currentTime) {
// Sample new data (replace with actual sensor reading)
float a = (sin(currentTime / 800.0) * 0.5 + 0.5); // Example: 0 to 1 sine wave (different freq)
aValues[aIndex] = a;
aIndex = (aIndex + 1) % GRAPH_POINTS;
displayNeedsUpdate = true; // Data changed
// Handle exit logic (same as W graph)
if (justEnteredScreen) {
if (encoderBtn.isReleased()) {
justEnteredScreen = false;
}
} else {
if (encoderBtn.isReleased()) {
currentScreen = SCREEN_MENU;
screenTransitionTime = currentTime;
displayNeedsUpdate = true;
}
}
}
// --- Drawing Functions ---
void drawMenu() {
// ----- Left side: Formula Circle -----
int cx = GRAPH_WIDTH + MENU_CIRCLE_X_OFFSET; // Centered relative to graph width
int cy = SCREEN_HEIGHT / 2;
display.drawCircle(cx, cy, MENU_CIRCLE_RADIUS, SSD1306_WHITE);
// Horizontal line (diameter)
display.drawLine(cx - MENU_CIRCLE_RADIUS + 1, cy, cx + MENU_CIRCLE_RADIUS - 1, cy, SSD1306_WHITE);
// Vertical line (radius)
display.drawLine(cx, cy, cx, cy + MENU_CIRCLE_RADIUS -1 , SSD1306_WHITE);
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
// Position text relative to center
display.setCursor(cx - 5, cy - 20); // V (Top)
display.print("V");
display.setCursor(cx - 15, cy + 5); // I (Bottom Left)
display.print("I"); // Using I for current as is common in formulas
display.setCursor(cx + 7, cy + 5); // R (Bottom Right)
display.print("R");
// ----- Right side: W/V/A Selection Boxes -----
int boxX = SCREEN_WIDTH - MENU_BOX_WIDTH + MENU_BOX_X_OFFSET;
int yMid = MENU_BOX_Y_TOP + MENU_BOX_HEIGHT;
int yBot = yMid + MENU_BOX_HEIGHT;
// W Box
bool isWSelected = (menuIndex == 0);
display.setTextColor(isWSelected ? SSD1306_BLACK : SSD1306_WHITE);
if (isWSelected && blinkState) { // Only fill selected box when blinking 'on'
display.fillRect(boxX, MENU_BOX_Y_TOP, MENU_BOX_WIDTH, MENU_BOX_HEIGHT, SSD1306_WHITE);
} else {
display.drawRect(boxX, MENU_BOX_Y_TOP, MENU_BOX_WIDTH, MENU_BOX_HEIGHT, SSD1306_WHITE);
}
display.setCursor(boxX + MENU_TEXT_X_OFFSET, MENU_BOX_Y_TOP + MENU_TEXT_Y_OFFSET);
display.print("W");
// V Box
bool isVSelected = (menuIndex == 1);
display.setTextColor(isVSelected ? SSD1306_BLACK : SSD1306_WHITE);
if (isVSelected && blinkState) {
display.fillRect(boxX, yMid, MENU_BOX_WIDTH, MENU_BOX_HEIGHT, SSD1306_WHITE);
} else {
display.drawRect(boxX, yMid, MENU_BOX_WIDTH, MENU_BOX_HEIGHT, SSD1306_WHITE);
}
display.setCursor(boxX + MENU_TEXT_X_OFFSET, yMid + MENU_TEXT_Y_OFFSET);
display.print("V");
// A Box
bool isASelected = (menuIndex == 2);
display.setTextColor(isASelected ? SSD1306_BLACK : SSD1306_WHITE);
if (isASelected && blinkState) {
display.fillRect(boxX, yBot, MENU_BOX_WIDTH, MENU_BOX_HEIGHT, SSD1306_WHITE);
} else {
display.drawRect(boxX, yBot, MENU_BOX_WIDTH, MENU_BOX_HEIGHT, SSD1306_WHITE);
}
display.setCursor(boxX + MENU_TEXT_X_OFFSET, yBot + MENU_TEXT_Y_OFFSET);
display.print("A");
// Restore default text color for subsequent draws if needed elsewhere
display.setTextColor(SSD1306_WHITE);
}
void drawWGraphScreen() {
// Draw graph area
drawGraphAxes(GRAPH_WIDTH, GRAPH_HEIGHT);
drawGraphData(wValues, wIndex, GRAPH_POINTS, GRAPH_WIDTH, GRAPH_HEIGHT);
// Draw back arrow (blinks when button not held after entry)
drawBackArrowBox(!justEnteredScreen && blinkState);
// --- Right Panel: Info ---
// Get current values (replace with actual sensor reads/calculations)
unsigned long currentTime = millis();
float v = 3.45 + 0.5 * sin(currentTime / 1500.0); // Example V
float a = 2.10 + 0.3 * cos(currentTime / 1200.0); // Example A
float w = v * a; // Calculated W
drawInfoPanel(w, v, a, "W =", "V x A", 0); // 0 indicates W is primary
}
void drawVGraphScreen() {
// Draw graph area
drawGraphAxes(GRAPH_WIDTH, GRAPH_HEIGHT);
drawGraphData(vValues, vIndex, GRAPH_POINTS, GRAPH_WIDTH, GRAPH_HEIGHT);
// Draw back arrow
drawBackArrowBox(!justEnteredScreen && blinkState);
// --- Right Panel: Info ---
unsigned long currentTime = millis();
float w = 7.00 + 2.0 * sin(currentTime / 1500.0); // Example W
float a = 2.10 + 0.3 * cos(currentTime / 1200.0); // Example A
float v = (a != 0) ? w / a : 0; // Calculated V
drawInfoPanel(w, v, a, "V =", "W / A", 1); // 1 indicates V is primary
}
void drawAGraphScreen() {
// Draw graph area
drawGraphAxes(GRAPH_WIDTH, GRAPH_HEIGHT);
drawGraphData(aValues, aIndex, GRAPH_POINTS, GRAPH_WIDTH, GRAPH_HEIGHT);
// Draw back arrow
drawBackArrowBox(!justEnteredScreen && blinkState);
// --- Right Panel: Info ---
unsigned long currentTime = millis();
float w = 7.00 + 2.0 * sin(currentTime / 1500.0); // Example W
float v = 3.45 + 0.5 * cos(currentTime / 1200.0); // Example V
float a = (v != 0) ? w / v : 0; // Calculated A
drawInfoPanel(w, v, a, "A =", "W / V", 2); // 2 indicates A is primary
}
// --- Drawing Helper Functions ---
void drawGraphAxes(uint8_t graphWidth, uint8_t graphHeight) {
// Draw border/box for the graph area
display.drawRect(0, 0, graphWidth, graphHeight, SSD1306_WHITE);
// Explicit X axis line (optional, as rect bottom edge covers it)
// display.drawLine(0, graphHeight - 1, graphWidth - 1, graphHeight - 1, SSD1306_WHITE);
// Explicit Y axis line (optional, as rect left edge covers it)
// display.drawLine(0, 0, 0, graphHeight - 1, SSD1306_WHITE);
}
void drawGraphData(float values[], uint8_t index, uint8_t points, uint8_t graphWidth, uint8_t graphHeight) {
int innerHeight = graphHeight - 2; // Available vertical pixels inside axes
for (int i = 1; i < points && i < graphWidth; i++) { // Ensure we don't draw outside width
// Calculate buffer indices, wrapping around
int idx0 = (index + i - 1 + points) % points;
int idx1 = (index + i + points) % points;
// Scale float value (0.0 to 1.0) to pixel coordinates
// Y=0 is top, Y=graphHeight-1 is bottom. Subtract from max Y.
// Clamp values just in case they go slightly out of 0-1 range
float val0 = constrain(values[idx0], 0.0, 1.0);
float val1 = constrain(values[idx1], 0.0, 1.0);
int y0 = (graphHeight - 1) - (int)(val0 * innerHeight);
int y1 = (graphHeight - 1) - (int)(val1 * innerHeight);
// Draw line segment - use graphWidth-1 as max X coordinate
display.drawLine(i - 1, y0, i, y1, SSD1306_WHITE);
}
}
void drawBackArrowBox(bool blink) {
int boxX = SCREEN_WIDTH - BACK_ARROW_BOX_WIDTH + BACK_ARROW_BOX_X_OFFSET;
if (blink) {
display.fillRect(boxX, BACK_ARROW_BOX_Y, BACK_ARROW_BOX_WIDTH, BACK_ARROW_BOX_HEIGHT, SSD1306_WHITE);
} else {
display.drawRect(boxX, BACK_ARROW_BOX_Y, BACK_ARROW_BOX_WIDTH, BACK_ARROW_BOX_HEIGHT, SSD1306_WHITE);
}
display.setTextSize(2);
display.setTextColor(blink ? SSD1306_BLACK : SSD1306_WHITE);
display.setCursor(boxX + BACK_ARROW_TEXT_X_OFFSET, BACK_ARROW_BOX_Y + BACK_ARROW_TEXT_Y_OFFSET);
display.print(""); // Left arrow character ← (Check if your font supports this)
// If not, consider drawing lines: display.drawLine(...)
display.setTextColor(SSD1306_WHITE); // Restore default color
}
void drawInfoPanel(float w, float v, float a, const char* formulaLine1, const char* formulaLine2, uint8_t primaryValueIndex) {
int infoX = GRAPH_WIDTH + INFO_PANEL_X_OFFSET;
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// Formula Lines
display.setCursor(infoX, INFO_PANEL_LINE1_Y);
display.print(formulaLine1);
display.setCursor(infoX, INFO_PANEL_LINE2_Y);
display.print(formulaLine2);
// Calculated Values (W, V, A)
int valueY = INFO_PANEL_Y + INFO_PANEL_VALUE_Y_OFFSET;
// Wattage
display.setCursor(infoX, valueY);
display.print("W: ");
display.print(w, 2); // Print with 2 decimal places
// Voltage
valueY += INFO_PANEL_VALUE_LINE_SPACING;
display.setCursor(infoX, valueY);
display.print("V: ");
display.print(v, 2);
// Amperage
valueY += INFO_PANEL_VALUE_LINE_SPACING;
display.setCursor(infoX, valueY);
display.print("A: ");
display.print(a, 2);
}
Loading
ssd1306
ssd1306