/**
 * @file PWMSquareWaveVisualizer.ino
 * @brief PWM signal generator and OLED square wave visualizer
 * 
 * This program generates PWM signals with sequential duty cycles (25%, 50%, 75%, 100%)
 * at 1-second intervals and displays them as square waves on an SSD1306 OLED display.
 */
#include <U8glib.h>
#include <avr/pgmspace.h>
/** @brief OLED display instance using I2C interface */
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_NONE);
/** @brief PWM output pin number */
const uint8_t PWM_PIN = 9;
/** @brief Analog input pin number (for reference only) */
const uint8_t ANALOG_PIN = A0;
/** @brief PWM update interval in milliseconds */
const uint16_t PWM_UPDATE_INTERVAL_MS = 1000;
/** @brief PWM duty cycle values (0-255) stored in program memory */
const uint8_t PROGMEM dutyCycles[] = {64, 128, 191, 255};
/** @brief Number of duty cycle steps in sequence */
const uint8_t DUTY_CYCLE_COUNT = 4;
/** @brief Current PWM value being output */
uint8_t pwmValue = 0;
/** @brief Flag to track if redraw is needed (only updated when value changes) */
bool needsRedraw = true;
// Graph display constants
/** @brief Left margin of graph area */
const uint8_t GRAPH_X = 5;
/** @brief Top margin of graph area */
const uint8_t GRAPH_Y = 10;
/** @brief Width of graph area in pixels */
const uint8_t GRAPH_WIDTH = 118;
/** @brief Height of graph area in pixels */
const uint8_t GRAPH_HEIGHT = 40;
/** @brief Width of one square wave period in pixels */
const uint8_t WAVE_PERIOD = 20;
/**
 * @brief Checks if it's time to perform an update based on interval
 * 
 * @param lastUpdate Timestamp of last update
 * @param interval Time interval in milliseconds
 * @return true if update is due, false otherwise
 */
bool isUpdateDue(unsigned long lastUpdate, unsigned long interval)
{
    return (millis() - lastUpdate >= interval);
}
/**
 * @brief Updates timestamp to current time
 * 
 * @param lastUpdate Reference to timestamp variable to update
 */
void updateLastTime(unsigned long &lastUpdate)
{
    lastUpdate = millis();
}
/**
 * @brief Retrieves duty cycle value for specified index from program memory
 * 
 * @param index Duty cycle index to retrieve
 * @return uint8_t PWM value (0-255) for the specified index
 */
uint8_t getDutyCycle(uint8_t index)
{
    // Get value from PROGMEM to save RAM
    return pgm_read_byte(&dutyCycles[index % DUTY_CYCLE_COUNT]);
}
/**
 * @brief Advances to next index with wrap-around
 * 
 * @param index Current index
 * @return uint8_t Next index value
 */
uint8_t advanceIndex(uint8_t index)
{
    return (index + 1) % DUTY_CYCLE_COUNT;
}
/**
 * @brief Sets PWM output on specified pin
 * 
 * @param pin PWM-capable output pin
 * @param value Duty cycle value (0-255)
 */
void outputPWM(uint8_t pin, uint8_t value)
{
    analogWrite(pin, value);
}
/**
 * @brief Updates PWM signal using non-blocking timing
 * 
 * Controls the PWM signal sequence without using delays
 */
void updatePwmSignal()
{
    static unsigned long lastUpdate = 0;
    static uint8_t index = 0;
    uint8_t newPwmValue;
    if (isUpdateDue(lastUpdate, PWM_UPDATE_INTERVAL_MS))
    {
        newPwmValue = getDutyCycle(index);
        
        // Only update if value changed
        if (newPwmValue != pwmValue) {
            pwmValue = newPwmValue;
            outputPWM(PWM_PIN, pwmValue);
            needsRedraw = true; // Mark for OLED update only when value changes
        }
        
        index = advanceIndex(index);
        updateLastTime(lastUpdate);
    }
}
/**
 * @brief Draws coordinate axes for the square wave graph
 */
void drawGraphAxes()
{
    // y-axis
    u8g.drawLine(GRAPH_X, GRAPH_Y, GRAPH_X, GRAPH_Y + GRAPH_HEIGHT);
    // x-axis
    u8g.drawLine(GRAPH_X, GRAPH_Y + GRAPH_HEIGHT, GRAPH_X + GRAPH_WIDTH, GRAPH_Y + GRAPH_HEIGHT);
}
/**
 * @brief Draws a square wave based on PWM duty cycle
 * 
 * @param dutyCycle PWM duty cycle value (0-255)
 */
void drawSquareWave(uint8_t dutyCycle)
{
    // Calculate high portion width based on duty cycle
    uint8_t highWidth = map(dutyCycle, 0, 255, 0, WAVE_PERIOD);
    uint8_t numPeriods = GRAPH_WIDTH / WAVE_PERIOD;
    
    for (uint8_t i = 0; i < numPeriods; i++)
    {
        uint8_t startX = GRAPH_X + i * WAVE_PERIOD;
        
        if (highWidth > 0)
        {
            // Rising edge
            u8g.drawLine(startX, GRAPH_Y + GRAPH_HEIGHT, startX, GRAPH_Y);
            // High level
            u8g.drawLine(startX, GRAPH_Y, startX + highWidth, GRAPH_Y);
            // Falling edge
            u8g.drawLine(startX + highWidth, GRAPH_Y, startX + highWidth, GRAPH_Y + GRAPH_HEIGHT);
        }
        // Low level
        u8g.drawLine(startX + highWidth, GRAPH_Y + GRAPH_HEIGHT, startX + WAVE_PERIOD, GRAPH_Y + GRAPH_HEIGHT);
    }
}
/**
 * @brief Renders PWM value as text on the OLED
 * 
 * @param dutyCycle PWM duty cycle value (0-255)
 */
void drawPwmText(uint8_t dutyCycle)
{
    char buf[16]; // Buffer for text
    uint8_t percentage = map(dutyCycle, 0, 255, 0, 100);
    
    u8g.setFont(u8g_font_6x10);
    snprintf(buf, sizeof(buf), "PWM: %d (%d%%)", dutyCycle, percentage);
    u8g.drawStr(GRAPH_X, GRAPH_Y - 2, buf);
}
/**
 * @brief Updates the OLED display with square wave visualization
 * 
 * Only redraws the screen when PWM value has changed to reduce MCU load
 * and minimize I2C communication overhead
 */
void updateDisplay()
{
    // Only redraw if needed (when PWM value changes)
    if (needsRedraw) {
        u8g.firstPage();
        do {
            drawGraphAxes();
            drawSquareWave(pwmValue);
            drawPwmText(pwmValue);
        } while (u8g.nextPage());
        
        needsRedraw = false; // Reset flag after redraw
    }
}
/**
 * @brief Arduino setup function - initializes pins and hardware
 */
void setup()
{
    pinMode(PWM_PIN, OUTPUT);
    pinMode(ANALOG_PIN, INPUT);
}
/**
 * @brief Arduino main loop function
 */
void loop()
{
    updatePwmSignal();
    updateDisplay();
}