// https://wokwi.com/projects/429040929775531009
// 주석은 코딩을 바꾸는 와중에도 지우지 말것!
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define ENCODER_PIN_A 2
#define ENCODER_PIN_B 3
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
volatile long encoderPosition = 0;
volatile int lastEncoded = 0;
int menuIndex = 0; // 0:W, 1:V, 2:A
unsigned long lastBlink = 0;
bool blinkState = false;
const unsigned long blinkInterval = 500; // 0.5초
// IRAM_ATTR는 ESP32/ESP8266에서만 필요합니다.
// 아두이노 UNO, Nano 등 AVR 계열에서는 void handleEncoder()로 선언해야 합니다.
void handleEncoder()
{
int MSB = digitalRead(ENCODER_PIN_A);
int LSB = digitalRead(ENCODER_PIN_B);
int encoded = (MSB << 1) | LSB;
int sum = (lastEncoded << 2) | encoded;
if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011)
encoderPosition++;
if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000)
encoderPosition--;
lastEncoded = encoded;
}
void drawMenu()
{
display.clearDisplay();
// ----- 왼쪽 공식 그림 -----
int x_offset = -28;
int cx = 64 + x_offset, cy = 32, r = 30;
display.drawCircle(cx, cy, r, SSD1306_WHITE);
display.drawLine(cx - 29, cy, cx + 29, cy, SSD1306_WHITE);
display.drawLine(cx, cy, cx, cy + 30, SSD1306_WHITE);
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.setCursor(cx - 5, cy - 20);
display.print("V");
display.setCursor(cx - 15, cy + 5);
display.print("I");
display.setCursor(cx + 7, cy + 5);
display.print("R");
// ----- 오른쪽 W/V/A 박스 -----
int box_height = 20;
int box_width = 24;
int box_x_offset = -5;
int text_x_offset = 7;
int text_y_offset = 2;
int x = SCREEN_WIDTH - box_width - 4 + box_x_offset;
int y_top = 2;
int y_mid = y_top + box_height;
int y_bot = y_mid + box_height;
// W 박스
if (menuIndex == 0 && blinkState)
display.fillRect(x, y_top, box_width, box_height, SSD1306_WHITE);
else
display.drawRect(x, y_top, box_width, box_height, SSD1306_WHITE);
display.setTextColor((menuIndex == 0 && blinkState) ? SSD1306_BLACK : SSD1306_WHITE);
display.setCursor(x + text_x_offset, y_top + text_y_offset);
display.print("W");
// V 박스
if (menuIndex == 1 && blinkState)
display.fillRect(x, y_mid, box_width, box_height, SSD1306_WHITE);
else
display.drawRect(x, y_mid, box_width, box_height, SSD1306_WHITE);
display.setTextColor((menuIndex == 1 && blinkState) ? SSD1306_BLACK : SSD1306_WHITE);
display.setCursor(x + text_x_offset, y_mid + text_y_offset);
display.print("V");
// A 박스
if (menuIndex == 2 && blinkState)
display.fillRect(x, y_bot, box_width, box_height, SSD1306_WHITE);
else
display.drawRect(x, y_bot, box_width, box_height, SSD1306_WHITE);
display.setTextColor((menuIndex == 2 && blinkState) ? SSD1306_BLACK : SSD1306_WHITE);
display.setCursor(x + text_x_offset, y_bot + text_y_offset);
display.print("A");
display.display();
display.setTextColor(SSD1306_WHITE); // 다음 draw를 위해 복원
}
void setup()
{
pinMode(ENCODER_PIN_A, INPUT_PULLUP);
pinMode(ENCODER_PIN_B, INPUT_PULLUP);
int MSB = digitalRead(ENCODER_PIN_A);
int LSB = digitalRead(ENCODER_PIN_B);
lastEncoded = (MSB << 1) | LSB;
attachInterrupt(digitalPinToInterrupt(ENCODER_PIN_A), handleEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_PIN_B), handleEncoder, CHANGE);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
drawMenu();
}
void loop()
{
static long lastPos = 0;
if (encoderPosition != lastPos)
{
int diff = encoderPosition - lastPos;
int step = diff / 4; // 엔코더 4틱마다 한 칸 이동
if (step != 0)
{
menuIndex -= step;
if (menuIndex < 0)
menuIndex = 0;
if (menuIndex > 2)
menuIndex = 2;
lastPos += step * 4; // 처리한 만큼 lastPos 갱신
drawMenu();
}
}
if (millis() - lastBlink > 500)
{
blinkState = !blinkState;
lastBlink = millis();
drawMenu();
}
}