#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ezButton.h>
#define ENCODER_PIN_A 2
#define ENCODER_PIN_B 3
#define ENCODER_BTN_PIN 4
#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
const unsigned long blinkInterval = 500; // 0.5초
ezButton encoderBtn(ENCODER_BTN_PIN);
// W 그래프 관련 변수 선언
#define W_GRAPH_WIDTH (SCREEN_WIDTH / 2)
#define W_GRAPH_HEIGHT SCREEN_HEIGHT
#define W_GRAPH_POINTS 64
float wValues[W_GRAPH_POINTS] = {0};
int wIndex = 0;
bool inWGraphScreen = false;
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)
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) ? SSD1306_BLACK : SSD1306_WHITE);
display.setCursor(x + text_x_offset, y_top + text_y_offset);
display.print("W");
// V 박스
if (menuIndex == 1)
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) ? SSD1306_BLACK : SSD1306_WHITE);
display.setCursor(x + text_x_offset, y_mid + text_y_offset);
display.print("V");
// A 박스
if (menuIndex == 2)
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) ? 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 drawWGraphScreen(bool blinkBackArrow)
{
display.clearDisplay();
// X,Y축 그리기
display.drawRect(0, 0, W_GRAPH_WIDTH, W_GRAPH_HEIGHT, SSD1306_WHITE);
display.drawLine(0, W_GRAPH_HEIGHT - 1, W_GRAPH_WIDTH, W_GRAPH_HEIGHT - 1, SSD1306_WHITE); // X축
display.drawLine(0, 0, 0, W_GRAPH_HEIGHT, SSD1306_WHITE); // Y축
// W 값 라인그래프 그리기
for (int i = 1; i < W_GRAPH_POINTS; i++)
{
int idx0 = (wIndex + i - 1) % W_GRAPH_POINTS;
int idx1 = (wIndex + i) % W_GRAPH_POINTS;
int y0 = W_GRAPH_HEIGHT - 1 - (int)(wValues[idx0] * (W_GRAPH_HEIGHT - 2));
int y1 = W_GRAPH_HEIGHT - 1 - (int)(wValues[idx1] * (W_GRAPH_HEIGHT - 2));
display.drawLine(i - 1, y0, i, y1, SSD1306_WHITE);
}
// 오른쪽 맨 위에 뒤로가기 화살표와 네모, 색상 반전
int boxW = 18, boxH = 18;
int boxX = SCREEN_WIDTH - boxW - 2;
int boxY = 2;
if (blinkBackArrow)
display.fillRect(boxX, boxY, boxW, boxH, SSD1306_WHITE);
else
display.drawRect(boxX, boxY, boxW, boxH, SSD1306_WHITE);
display.setTextSize(2);
display.setTextColor(blinkBackArrow ? SSD1306_BLACK : SSD1306_WHITE);
display.setCursor(boxX + 3, boxY + 2);
display.print("\x1B"); // ←
display.setTextColor(SSD1306_WHITE); // 복원
// --- 오른쪽 상단에 실시간 계산식 및 값 표시 ---
float v = 3.45 + 0.5 * sin(millis() / 1500.0); // 예시 V
float a = 2.10 + 0.3 * cos(millis() / 1200.0); // 예시 A
float w = v * a;
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
int infoX = W_GRAPH_WIDTH + 4;
int infoY = 24; // 더 위로 올림
// 계산식 2줄로 표시
display.setCursor(infoX, 4);
display.print("W =");
display.setCursor(infoX, 16);
display.print("V x A");
// 수치 표시
display.setCursor(infoX, infoY + 6);
display.print("W: "); display.print(w, 2);
display.setCursor(infoX, infoY + 18);
display.print("V: "); display.print(v, 2);
display.setCursor(infoX, infoY + 30);
display.print("A: "); display.print(a, 2);
display.display();
}
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();
encoderBtn.setDebounceTime(50); // 50ms 디바운스
}
void loop()
{
static long lastPos = 0;
static unsigned long lastBlink = 0;
static bool blinkState = false;
static unsigned long wScreenBtnPressedTime = 0;
static bool wScreenEntered = false;
encoderBtn.loop();
// 메뉴에서 버튼 눌림 감지 (ezButton 사용)
if (!inWGraphScreen && menuIndex == 0 && encoderBtn.isPressed())
{
inWGraphScreen = true;
wScreenEntered = true; // 진입 직후
display.clearDisplay();
display.display();
}
// 메뉴 화면에서 엔코더 회전 처리
if (!inWGraphScreen) {
long filteredEncoderPos = encoderPosition / 4;
if (filteredEncoderPos != lastPos) {
if (filteredEncoderPos > lastPos) {
menuIndex = (menuIndex + 1) % 3;
} else {
menuIndex = (menuIndex + 2) % 3; // (menuIndex-1+3)%3
}
lastPos = filteredEncoderPos;
drawMenu();
}
// 깜빡임 타이머
if (millis() - lastBlink > blinkInterval) {
blinkState = !blinkState;
lastBlink = millis();
drawMenu();
}
}
if (inWGraphScreen) {
// 깜빡임 타이머
if (millis() - lastBlink > blinkInterval) {
blinkState = !blinkState;
lastBlink = millis();
}
// W값 샘플링 (여기선 예시로 사인파)
float w = (sin(millis() / 1000.0) + 1.0) / 2.0; // 0~1 범위
wValues[wIndex] = w;
wIndex = (wIndex + 1) % W_GRAPH_POINTS;
drawWGraphScreen(blinkState);
// 진입 직후에는 버튼이 완전히 떼어질 때까지 복귀 처리 금지
if (wScreenEntered) {
if (encoderBtn.isReleased()) {
wScreenEntered = false;
}
} else {
// 버튼 눌렀다 떼면 복귀
if (encoderBtn.isPressed()) {
wScreenBtnPressedTime = millis();
}
if (encoderBtn.isReleased()) {
inWGraphScreen = false;
drawMenu();
delay(300); // 복귀시 디바운스
}
}
return;
}
}