#include <U8glib.h>
const uint8_t PWM_OUTPUT_PIN = 9;
const uint8_t PWM_FEEDBACK_PIN = A0;
const uint8_t CONTROL_PIN_UP = 2;
const uint8_t CONTROL_PIN_DOWN = 3;
const uint16_t CONTROL_UPDATE_INTERVAL_MS = 100;
const uint8_t PWM_FEEDBACK_SAMPLE_COUNT = 5;
const uint8_t PWM_MIN_DUTY_CYCLE = 0;
const uint8_t PWM_MAX_DUTY_CYCLE = 255;
const uint8_t PWM_DUTY_CYCLE_STEP = 8;
const uint16_t ADC_SAMPLE_DELAY_US = 50;
const float EMA_FILTER_ALPHA = 0.15f;
const uint8_t GRAPH_ORIGIN_X = 5;
const uint8_t GRAPH_ORIGIN_Y = 10;
const uint8_t GRAPH_WIDTH = 118;
const uint8_t GRAPH_HEIGHT = 40;
const uint8_t GRAPH_WAVE_PERIOD_PIXELS = 20;
const int8_t TEXT_SET_Y_OFFSET = -2;
const int8_t TEXT_MEASURE_Y_OFFSET = 10;
const uint8_t TEXT_PERCENTAGE_X_OFFSET = 75;
const uint8_t TEXT_BUFFER_SIZE = 30;
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_DEV_0 | U8G_I2C_OPT_NO_ACK | U8G_I2C_OPT_FAST);
uint8_t gTargetPwmValue = 127;
uint8_t gDisplayMeasuredPwmValue = 127;
float gFilteredMeasuredPwmFloat = 127.0f;
uint8_t gLastDisplayedMeasuredValue = 127;
bool isUpdateIntervalPassed(unsigned long lastUpdateTime, uint16_t intervalMs)
{
    return (millis() - lastUpdateTime >= intervalMs);
}
void recordUpdateTime(unsigned long &lastUpdateTime)
{
    lastUpdateTime = millis();
}
uint8_t readRawPwmFeedback()
{
    long adcTotal = 0;
    for (uint8_t i = 0; i < PWM_FEEDBACK_SAMPLE_COUNT; i++)
    {
        int adcReading = analogRead(PWM_FEEDBACK_PIN);
        adcTotal += adcReading;
        delayMicroseconds(ADC_SAMPLE_DELAY_US);
    }
    int averageAdcReading = adcTotal / PWM_FEEDBACK_SAMPLE_COUNT;
    averageAdcReading = constrain(averageAdcReading, 0, 1023);
    return map(averageAdcReading, 0, 1023, PWM_MIN_DUTY_CYCLE, PWM_MAX_DUTY_CYCLE);
}
void applyEmaFilter(uint8_t rawMeasurement)
{
    gFilteredMeasuredPwmFloat = (EMA_FILTER_ALPHA * (float)rawMeasurement) +
                                ((1.0f - EMA_FILTER_ALPHA) * gFilteredMeasuredPwmFloat);
    gDisplayMeasuredPwmValue = (uint8_t)(gFilteredMeasuredPwmFloat + 0.5f);
    gDisplayMeasuredPwmValue = constrain(gDisplayMeasuredPwmValue, PWM_MIN_DUTY_CYCLE, PWM_MAX_DUTY_CYCLE);
}
void setPwmOutput(uint8_t dutyCycle)
{
    analogWrite(PWM_OUTPUT_PIN, dutyCycle);
}
bool updateTargetPwmFromControls()
{
    static unsigned long lastControlCheckTime = 0;
    static uint8_t previousTargetPwmValue = gTargetPwmValue;
    bool valueChanged = false;
    if (isUpdateIntervalPassed(lastControlCheckTime, CONTROL_UPDATE_INTERVAL_MS))
    {
        int pinStateUp = digitalRead(CONTROL_PIN_UP);
        int pinStateDown = digitalRead(CONTROL_PIN_DOWN);
        previousTargetPwmValue = gTargetPwmValue;
        if (pinStateUp == LOW && pinStateDown == HIGH)
        {
            if (gTargetPwmValue < PWM_MAX_DUTY_CYCLE)
            {
                gTargetPwmValue = min((int)PWM_MAX_DUTY_CYCLE, (int)gTargetPwmValue + PWM_DUTY_CYCLE_STEP);
            }
        }
        else if (pinStateDown == LOW && pinStateUp == HIGH)
        {
            if (gTargetPwmValue > PWM_MIN_DUTY_CYCLE)
            {
                gTargetPwmValue = max((int)PWM_MIN_DUTY_CYCLE, (int)gTargetPwmValue - PWM_DUTY_CYCLE_STEP);
            }
        }
        if (gTargetPwmValue != previousTargetPwmValue)
        {
            valueChanged = true;
            setPwmOutput(gTargetPwmValue);
        }
        recordUpdateTime(lastControlCheckTime);
    }
    return valueChanged;
}
void drawGraphAxes()
{
    u8g.drawLine(GRAPH_ORIGIN_X, GRAPH_ORIGIN_Y, GRAPH_ORIGIN_X, GRAPH_ORIGIN_Y + GRAPH_HEIGHT);
    u8g.drawLine(GRAPH_ORIGIN_X, GRAPH_ORIGIN_Y + GRAPH_HEIGHT, GRAPH_ORIGIN_X + GRAPH_WIDTH, GRAPH_ORIGIN_Y + GRAPH_HEIGHT);
}
void drawSquareWaveGraph(uint8_t dutyCycle)
{
    uint8_t highWidthPixels = map(dutyCycle, PWM_MIN_DUTY_CYCLE, PWM_MAX_DUTY_CYCLE, 0, GRAPH_WAVE_PERIOD_PIXELS);
    uint8_t numPeriods = GRAPH_WIDTH / GRAPH_WAVE_PERIOD_PIXELS;
    const uint8_t yHigh = GRAPH_ORIGIN_Y;
    const uint8_t yLow = GRAPH_ORIGIN_Y + GRAPH_HEIGHT;
    for (uint8_t i = 0; i < numPeriods; i++)
    {
        uint8_t startX = GRAPH_ORIGIN_X + i * GRAPH_WAVE_PERIOD_PIXELS;
        uint8_t endHighX = startX + highWidthPixels;
        uint8_t endPeriodX = startX + GRAPH_WAVE_PERIOD_PIXELS;
        if (highWidthPixels > 0)
        {
            u8g.drawLine(startX, yLow, startX, yHigh);
            u8g.drawLine(startX, yHigh, endHighX, yHigh);
            if (highWidthPixels < GRAPH_WAVE_PERIOD_PIXELS)
            {
                u8g.drawLine(endHighX, yHigh, endHighX, yLow);
            }
        }
        u8g.drawLine(endHighX, yLow, endPeriodX, yLow);
    }
}
void drawPwmInfoText()
{
    char textBuffer[TEXT_BUFFER_SIZE];
    uint8_t setPercentage = map(gTargetPwmValue, PWM_MIN_DUTY_CYCLE, PWM_MAX_DUTY_CYCLE, 0, 100);
    uint8_t measuredPercentage = map(gDisplayMeasuredPwmValue, PWM_MIN_DUTY_CYCLE, PWM_MAX_DUTY_CYCLE, 0, 100);
    u8g.setFont(u8g_font_6x10);
    snprintf(textBuffer, TEXT_BUFFER_SIZE, "Set: %d", gTargetPwmValue);
    u8g.drawStr(GRAPH_ORIGIN_X, GRAPH_ORIGIN_Y + TEXT_SET_Y_OFFSET, textBuffer);
    snprintf(textBuffer, TEXT_BUFFER_SIZE, "(%d%%)", setPercentage);
    u8g.drawStr(GRAPH_ORIGIN_X + TEXT_PERCENTAGE_X_OFFSET, GRAPH_ORIGIN_Y + TEXT_SET_Y_OFFSET, textBuffer);
    snprintf(textBuffer, TEXT_BUFFER_SIZE, "Measure: %d", gDisplayMeasuredPwmValue);
    u8g.drawStr(GRAPH_ORIGIN_X, GRAPH_ORIGIN_Y + GRAPH_HEIGHT + TEXT_MEASURE_Y_OFFSET, textBuffer);
    snprintf(textBuffer, TEXT_BUFFER_SIZE, "(%d%%)", measuredPercentage);
    u8g.drawStr(GRAPH_ORIGIN_X + TEXT_PERCENTAGE_X_OFFSET, GRAPH_ORIGIN_Y + GRAPH_HEIGHT + TEXT_MEASURE_Y_OFFSET, textBuffer);
}
void renderOledDisplay()
{
    u8g.firstPage();
    do
    {
        drawGraphAxes();
        drawSquareWaveGraph(gTargetPwmValue);
        drawPwmInfoText();
    } while (u8g.nextPage());
}
void setup()
{
    pinMode(PWM_OUTPUT_PIN, OUTPUT);
    pinMode(CONTROL_PIN_UP, INPUT_PULLUP);
    pinMode(CONTROL_PIN_DOWN, INPUT_PULLUP);
    pinMode(PWM_FEEDBACK_PIN, INPUT);
    analogReference(DEFAULT);
    gFilteredMeasuredPwmFloat = (float)gTargetPwmValue;
    gDisplayMeasuredPwmValue = gTargetPwmValue;
    gLastDisplayedMeasuredValue = gTargetPwmValue;
    setPwmOutput(gTargetPwmValue);
}
void loop()
{
    uint8_t rawMeasurement = readRawPwmFeedback();
    applyEmaFilter(rawMeasurement);
    bool targetPwmChanged = updateTargetPwmFromControls();
    bool displayUpdateNeeded = targetPwmChanged || (gDisplayMeasuredPwmValue != gLastDisplayedMeasuredValue);
    if (displayUpdateNeeded)
    {
        renderOledDisplay();
        gLastDisplayedMeasuredValue = gDisplayMeasuredPwmValue;
    }
}