Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flicker issue is not smooth when blinking #208

Open
BimoSora2 opened this issue Jan 23, 2025 · 0 comments
Open

Flicker issue is not smooth when blinking #208

BimoSora2 opened this issue Jan 23, 2025 · 0 comments

Comments

@BimoSora2
Copy link

I hope you fix the library to overcome flicker like this which is clearly not smooth.

VID-20250124-WA0000.mp4
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <Adafruit_BMP280.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_INA219.h>
#include <SPI.h>
#include <Wire.h>
#include <LittleFS.h>
#include <TimeLib.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <DNSServer.h>
#include <ArduinoJson.h>
#include <Fonts/Swansea5pt7b.h>
#include <Fonts/Swansea6pt7b.h>
#include <Fonts/Open_24_Display_St17pt7b.h>

const char* ssid = "Time";         
const char* password = "12345678";    

#define TFT_MOSI  13
#define TFT_SCLK  14
#define TFT_DC    0
#define downButton 12

const int BATT_IMG_WIDTH = 22;
const int BATT_IMG_HEIGHT = 10;
const int ICON_WIDTH = 80;
const int ICON_HEIGHT = 80;

const int SMALL_ICON_WIDTH = 32;
const int SMALL_ICON_HEIGHT = 32;

const char* ICON_TEMP = "/temp_icon.bmp";
const char* ICON_PRESSURE = "/pressure_icon.bmp";
const char* ICON_ALTITUDE = "/altitude_icon.bmp";
const char* ICON_ANGLE = "/angle_icon.bmp";
const char* ICON_STEP = "/step_icon.bmp";
const char* ICON_SETTINGS = "/settings_icon.bmp";

const byte DNS_PORT = 53;
DNSServer dnsServer;
IPAddress apIP(172, 217, 28, 1);

ESP8266WebServer server(80);
Adafruit_ST7735 tft = Adafruit_ST7735(-1, TFT_DC, TFT_MOSI, TFT_SCLK, -1);
Adafruit_BMP280 bmp;
Adafruit_MPU6050 mpu;
Adafruit_INA219 ina219;

bool bmpEnabled = false;
bool mpuEnabled = false;

const float BATTERY_MAX = 4.2;
const float BATTERY_MIN = 3.0;

float temperature = 0, pressure = 0, altitude = 0;
float lastPressure = 0;
String tempC = "0.0 *C", tempF = "0.0 *F", pressureStr = "0.0 hPa", altitudeStr = "0.0 m";
String accX = "X: 0*", accY = "Y: 0*", accZ = "Z: 0*";
String batteryStatus = "100%";
String prevBatteryStatus = "";
String lastVoltageStr = "";
String prevTempC, prevTempF, prevPressureStr, prevAltitudeStr;
String prevAccX = "", prevAccY = "", prevAccZ = "";
String timeStr = "00:00";
String dateStr = "00/00/0000";
String prevTimeStr = "";
String prevDateStr = "";
String settingsText = "SETTING";
String prevSettingsText = "";
uint16_t backgroundColor = 0;
const uint16_t w = 80;
const uint16_t h = 160;

const int CONTENT_MARGIN = 9;  // Margin untuk semua konten (left, right, top, bottom)
const int BORDER_WIDTH = 80;   // Lebar border utama
const int BORDER_HEIGHT = 160; // Tinggi border utama

// Konstanta turunan untuk posisi border
const int BORDER_START_X = (tft.width() - BORDER_WIDTH) / 2;
const int BORDER_START_Y = (tft.height() - BORDER_HEIGHT) / 2;

unsigned long stepCount = 0;
String prevStepCount = "";
bool isFirstRun = true;

bool autoReturnEnabled = true;

bool bmpAvailable = false;
bool mpuAvailable = false;
bool ina219Available = false;
bool isInverted = true;
bool wifiInitialized = false;
volatile bool buttonPressed = false;
bool iconsDrawnTimeMenu = false;
int currentMenu = 1;

unsigned long lastButtonPress = 0;
const unsigned long AUTO_RETURN_TIMEOUT = 20000;

unsigned long buttonPressStartTime = 0;
const unsigned long LONG_PRESS_DURATION = 800;
const unsigned long DEBOUNCE_TIME = 0;
bool isButtonDown = false;
volatile bool longPressExecuted = false;

unsigned long sensorMillis = 0;
unsigned long stepMillis = 0;
unsigned long tempMillis = 0;
unsigned long pressureMillis = 0;
unsigned long altitudeMillis = 0;
const long sensorInterval = 50;
const long stepInterval = 1000;
const long tempInterval = 4000;
const long pressureInterval = 4000;
const long altitudeInterval = 4000;

const int WINDOW_SIZE = 10;  
float accelWindow[WINDOW_SIZE];
int windowIndex = 0;
unsigned long lastStepTime = 0;
const unsigned long MIN_STEP_INTERVAL = 150;
const float VARIANCE_THRESHOLD = 0.5;
const float STEP_MAGNITUDE_THRESHOLD = 1.5;
const float PEAK_THRESHOLD = 2.0;

class KalmanFilter {
private:
    float Q; // Process noise variance
    float R; // Measurement noise variance
    float P; // Estimation error variance
    float X; // State estimate
    float K; // Kalman gain
    bool initialized;

public:
    KalmanFilter(float processNoise = 0.001, float measurementNoise = 0.1) :
        Q(processNoise),
        R(measurementNoise),
        P(1.0),
        X(0),
        K(0),
        initialized(false) {}

    float update(float measurement) {
        if (!initialized) {
            X = measurement;
            initialized = true;
            return X;
        }

        P = P + Q;
        K = P / (P + R);
        X = X + K * (measurement - X);
        P = (1 - K) * P;

        return X;
    }

    void reset() {
        initialized = false;
        P = 1.0;
        X = 0;
        K = 0;
    }
};

// Then modify the KalmanFilter class declaration and other code before these functions
KalmanFilter tempKalman(0.001, 0.1);
KalmanFilter pressureKalman(0.01, 1.0);
KalmanFilter altitudeKalman(0.01, 2.0);
KalmanFilter accelXKalman(0.01, 0.5);
KalmanFilter accelYKalman(0.01, 0.5);
KalmanFilter accelZKalman(0.01, 0.5);
KalmanFilter gyroXKalman(0.01, 0.1);
KalmanFilter gyroYKalman(0.01, 0.1);
KalmanFilter gyroZKalman(0.01, 0.1);

struct FilteredAngles {
    float roll;  // X-axis rotation
    float pitch; // Y-axis rotation
    float yaw;   // Z-axis rotation
};

float gyroAngleX = 0;
float gyroAngleY = 0;
float gyroAngleZ = 0;
unsigned long lastGyroRead = 0;

struct FilteredMPUData {
    float x, y, z;
    float roll, pitch, yaw;  // Added these fields for angular data
};

float readFilteredTemperature() {
    if (!bmpAvailable || !bmpEnabled) return 0.0;
    float rawTemp = bmp.readTemperature();
    return tempKalman.update(rawTemp);
}

float readFilteredPressure() {
    if (!bmpAvailable || !bmpEnabled) return 0.0;
    float rawPressure = bmp.readPressure() / 100.0F;
    return pressureKalman.update(rawPressure);
}

float readFilteredAltitude() {
    if (!bmpAvailable || !bmpEnabled) return 0.0;
    float rawAltitude = bmp.readAltitude(1013.25);
    return altitudeKalman.update(rawAltitude);
}

FilteredMPUData readFilteredAcceleration() {
    FilteredMPUData filtered = {0, 0, 0, 0, 0, 0};
    if (!mpuAvailable || !mpuEnabled) return filtered;

    sensors_event_t a, g, temp;
    mpu.getEvent(&a, &g, &temp);

    // Calculate time elapsed since last reading
    unsigned long now = micros();
    float dt = (now - lastGyroRead) / 1000000.0;
    lastGyroRead = now;

    // Filter accelerometer data
    filtered.x = accelXKalman.update(a.acceleration.x);
    filtered.y = accelYKalman.update(a.acceleration.y);
    filtered.z = accelZKalman.update(a.acceleration.z);

    // Filter gyroscope data
    float gyroX = gyroXKalman.update(g.gyro.x);
    float gyroY = gyroYKalman.update(g.gyro.y);
    float gyroZ = gyroZKalman.update(g.gyro.z);

    // Calculate pitch and roll from accelerometer
    float pitch = atan2(-filtered.x, sqrt(filtered.y * filtered.y + filtered.z * filtered.z));
    float roll = atan2(filtered.y, filtered.z);

    // Convert to degrees
    filtered.pitch = pitch * 180.0 / M_PI;
    filtered.roll = roll * 180.0 / M_PI;

    /*
    // Calculate yaw using gyroscope with drift compensation
    static float yawAngle = 0.0;
    const float GYRO_THRESHOLD = 0.02; // Threshold untuk menghilangkan noise gyro
    
    if (abs(gyroZ) > GYRO_THRESHOLD) {
        yawAngle += gyroZ * dt;
        
        // Normalize yaw angle to -180 to +180 degrees
        while (yawAngle > 180) yawAngle -= 360;
        while (yawAngle < -180) yawAngle += 360;
    }
    
    filtered.yaw = yawAngle;
    */

    return filtered;
}


float calculateFilteredAngle(float ax, float ay, float az) {
    static float filteredX = 0;
    static float filteredY = 0;
    static float filteredZ = 0;
    
    // Complementary filter coefficient
    const float ALPHA = 0.96;
    
    // Calculate raw angles
    float roll = atan2(ay, az) * 180.0 / M_PI;
    float pitch = atan2(-ax, sqrt(ay * ay + az * az)) * 180.0 / M_PI;
    float yaw = atan2(ax, ay) * 180.0 / M_PI;
    
    // Apply complementary filter
    filteredX = ALPHA * filteredX + (1.0 - ALPHA) * roll;
    filteredY = ALPHA * filteredY + (1.0 - ALPHA) * pitch;
    filteredZ = ALPHA * filteredZ + (1.0 - ALPHA) * yaw;
    
    return filteredX; // Return filtered roll by default
}
void resetKalmanFilters() {
    tempKalman.reset();
    pressureKalman.reset();
    altitudeKalman.reset();
    accelXKalman.reset();
    accelYKalman.reset();
    accelZKalman.reset();
}

void enableBMP280() {
    if (!bmpEnabled && bmpAvailable) {
        bmp.setSampling(Adafruit_BMP280::MODE_NORMAL,
                       Adafruit_BMP280::SAMPLING_X2,
                       Adafruit_BMP280::SAMPLING_X16,
                       Adafruit_BMP280::FILTER_X4,
                       Adafruit_BMP280::STANDBY_MS_4000);
        bmpEnabled = true;
        Serial.println("BMP280 enabled");
    }
}

void disableBMP280() {
    if (bmpEnabled && bmpAvailable) {
        bmp.setSampling(Adafruit_BMP280::MODE_SLEEP,
                       Adafruit_BMP280::SAMPLING_NONE,
                       Adafruit_BMP280::SAMPLING_NONE,
                       Adafruit_BMP280::FILTER_OFF,
                       Adafruit_BMP280::STANDBY_MS_4000);
        bmpEnabled = false;
        Serial.println("BMP280 disabled");
    }
}

void enableMPU6050() {
    if (!mpuEnabled && mpuAvailable) {
        mpu.enableSleep(false);
        mpuEnabled = true;
        Serial.println("MPU6050 enabled");
    }
}

void disableMPU6050() {
    if (mpuEnabled && mpuAvailable) {
        mpu.enableSleep(true);
        mpuEnabled = false;
        Serial.println("MPU6050 disabled");
    }
}

void handleRoot() {
    File file = LittleFS.open("/index.html", "r");
    if (!file) {
        server.send(404, "text/plain", "File not found");
        return;
    }
    server.streamFile(file, "text/html");
    file.close();
}

void handleSetTime() {
    if (server.hasArg("plain")) {
        String json = server.arg("plain");
        StaticJsonDocument<200> doc;
        DeserializationError error = deserializeJson(doc, json);

        if (error) {
            server.send(400, "text/plain", "Invalid JSON");
            return;
        }

        int year = doc["year"];
        int month = doc["month"];
        int day = doc["day"];
        int hour = doc["hour"];
        int minute = doc["minute"];
        int second = doc["second"];

        setTime(hour, minute, second, day, month, year);
        
        server.send(200, "text/plain", "Time set successfully");
    } else {
        server.send(400, "text/plain", "No data received");
    }
}

void setupWebServer() {
    WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));
    dnsServer.start(DNS_PORT, "*", apIP);
    
    server.onNotFound([]() {
        server.sendHeader("Location", String("http://") + apIP.toString(), true);
        server.send(302, "text/plain", "");
    });

    server.on("/", HTTP_GET, handleRoot);
    server.on("/settime", HTTP_POST, handleSetTime);
    server.begin();
    Serial.println("HTTP server started");
}

uint16_t safeColor565(int r, int g, int b) {
    r = constrain(r, 0, 255);
    g = constrain(g, 0, 255);
    b = constrain(b, 0, 255);
    uint8_t r5 = round(r * 31.0 / 255.0);
    uint8_t g6 = round(g * 63.0 / 255.0);
    uint8_t b5 = round(b * 31.0 / 255.0);
    return (r5 << 11) | (g6 << 5) | b5;
}

void setBackgroundColor(int r, int g, int b) {
    backgroundColor = safeColor565(r, g, b);
}

void clearScreen() {
    tft.fillScreen(backgroundColor);
    drawMargins();
}

void drawMargins() {
    // Gambar margin kiri dan kanan dengan warna hitam
    tft.fillRect(0, 0, CONTENT_MARGIN, tft.height(), safeColor565(0, 0, 0));
    tft.fillRect(tft.width() - CONTENT_MARGIN, 0, CONTENT_MARGIN, tft.height(), safeColor565(0, 0, 0));
}

void drawBorder() {
    drawMargins();
    int borderStartX = (tft.width() - BORDER_WIDTH) / 2;
    int borderStartY = (tft.height() - BORDER_HEIGHT) / 2;
    tft.drawFastHLine(borderStartX, borderStartY, BORDER_WIDTH, safeColor565(0, 0, 0));
    tft.drawFastHLine(borderStartX, borderStartY + BORDER_HEIGHT, BORDER_WIDTH, safeColor565(0, 0, 0));
    tft.drawFastVLine(borderStartX, borderStartY, BORDER_HEIGHT, safeColor565(0, 0, 0));
    tft.drawFastVLine(borderStartX + BORDER_WIDTH, borderStartY, BORDER_HEIGHT, safeColor565(0, 0, 0));
}

void clearBorderArea() {
    // Hitung posisi battery icon
    const int battX = BORDER_START_X + BORDER_WIDTH - BATT_IMG_WIDTH - CONTENT_MARGIN;
    const int battY = BORDER_START_Y + CONTENT_MARGIN;
    
    // Clear area diatas battery (jika ada)
    tft.fillRect(BORDER_START_X + 1, BORDER_START_Y + 1, 
                 BORDER_WIDTH - 2, battY - BORDER_START_Y - 1, 
                 backgroundColor);
    
    // Clear area kiri battery
    tft.fillRect(BORDER_START_X + 1, battY, 
                 battX - BORDER_START_X - 1, BATT_IMG_HEIGHT, 
                 backgroundColor);
    
    // Clear area kanan battery
    tft.fillRect(battX + BATT_IMG_WIDTH + 1, battY,
                 BORDER_START_X + BORDER_WIDTH - (battX + BATT_IMG_WIDTH) - 1, 
                 BATT_IMG_HEIGHT,
                 backgroundColor);
    
    // Clear area dibawah battery
    tft.fillRect(BORDER_START_X + 1, battY + BATT_IMG_HEIGHT,
                 BORDER_WIDTH - 2, 
                 (BORDER_START_Y + BORDER_HEIGHT - 1) - (battY + BATT_IMG_HEIGHT),
                 backgroundColor);
}

int adjustContentY(int y) {
    return BORDER_START_Y + y;  // Sekarang relatif terhadap border
}

uint32_t read32(File &f) {
    uint32_t result;
    ((uint8_t *)&result)[0] = f.read();
    ((uint8_t *)&result)[1] = f.read();
    ((uint8_t *)&result)[2] = f.read();
    ((uint8_t *)&result)[3] = f.read();
    return result;
}

void drawBMP(const char *filename, int16_t x, int16_t y, int16_t targetWidth, int16_t targetHeight) {
    File bmpFile = LittleFS.open(filename, "r");
    if (!bmpFile) {
        Serial.println("Failed to open BMP file");
        return;
    }

    // Read BMP header
    bmpFile.seek(0x0A);
    uint32_t imageOffset = read32(bmpFile);
    bmpFile.seek(0x12);
    uint32_t imageWidth = read32(bmpFile);
    uint32_t imageHeight = read32(bmpFile);

    // Calculate scaling while preserving aspect ratio
    float scaleX = (float)targetWidth / imageWidth;
    float scaleY = (float)targetHeight / imageHeight;
    float scale = min(scaleX, scaleY);  // Use the smaller scaling factor
    
    int16_t actualWidth = round(imageWidth * scale);
    int16_t actualHeight = round(imageHeight * scale);
    
    // Center the image within the target area
    x += (targetWidth - actualWidth) / 2;
    y += (targetHeight - actualHeight) / 2;

    // Check if this is a small icon (32x32 or smaller)
    bool isSmallIcon = (targetWidth <= 32 && targetHeight <= 32);

    if (isSmallIcon) {
        // Pre-calculate scaling ratios
        float scaleX = (float)imageWidth / actualWidth;
        float scaleY = (float)imageHeight / actualHeight;
        
        // Create temporary buffer for a single line of pixels
        uint8_t* lineBuffer = new uint8_t[imageWidth * 4];
        
        for (int16_t targetY = 0; targetY < actualHeight; targetY++) {
            // Calculate source Y position
            float srcY = (actualHeight - 1 - targetY) * scaleY;
            int srcYInt = (int)srcY;
            
            // Read the full line of source pixels
            bmpFile.seek(imageOffset + (srcYInt * imageWidth * 4));
            bmpFile.read(lineBuffer, imageWidth * 4);
            
            for (int16_t targetX = 0; targetX < actualWidth; targetX++) {
                // Calculate source X position
                float srcX = targetX * scaleX;
                int srcXInt = (int)srcX;
                
                // Get color values from buffer
                uint8_t b = lineBuffer[srcXInt * 4];
                uint8_t g = lineBuffer[srcXInt * 4 + 1];
                uint8_t r = lineBuffer[srcXInt * 4 + 2];
                uint8_t a = lineBuffer[srcXInt * 4 + 3];

                if (a > 127) {  // Only draw if pixel is visible
                    // For small icons, enhance contrast and sharpness
                    r = enhancePixel(r);
                    g = enhancePixel(g);
                    b = enhancePixel(b);

                    uint16_t color = safeColor565(r, g, b);
                    
                    // Check boundaries before drawing
                    if (x + targetX >= BORDER_START_X && 
                        x + targetX < BORDER_START_X + BORDER_WIDTH &&
                        y + targetY >= BORDER_START_Y && 
                        y + targetY < BORDER_START_Y + BORDER_HEIGHT) {
                        
                        tft.drawPixel(x + targetX, y + targetY, color);
                        
                        // Apply edge smoothing
                        if (targetX > 0 && targetY > 0 && 
                            targetX < actualWidth-1 && targetY < actualHeight-1) {
                            if (isEdge(lineBuffer, srcXInt, imageWidth)) {
                                smoothEdge(tft, x + targetX, y + targetY, color);
                            }
                        }
                    }
                }
            }
        }
        
        delete[] lineBuffer;
    } else {
        // Original scaling method for larger images
        for (int16_t row = 0; row < actualHeight; row++) {
            int16_t sourceRow = imageHeight - 1 - (int16_t)((float)row * imageHeight / actualHeight);
            bmpFile.seek(imageOffset + (sourceRow * imageWidth * 4));

            for (int16_t col = 0; col < actualWidth; col++) {
                int16_t sourceCol = (int16_t)((float)col * imageWidth / actualWidth);
                bmpFile.seek(imageOffset + (sourceRow * imageWidth * 4) + (sourceCol * 4));

                uint8_t b = bmpFile.read();
                uint8_t g = bmpFile.read();
                uint8_t r = bmpFile.read();
                uint8_t a = bmpFile.read();

                if (a > 127) {
                    // Check boundaries before drawing
                    if (x + col >= BORDER_START_X && 
                        x + col < BORDER_START_X + BORDER_WIDTH &&
                        y + row >= BORDER_START_Y && 
                        y + row < BORDER_START_Y + BORDER_HEIGHT) {
                        
                        tft.drawPixel(x + col, y + row, safeColor565(r, g, b));
                    }
                }
            }
        }
    }
    
    bmpFile.close();
}

uint8_t enhancePixel(uint8_t value) {
    // Increase contrast for small icons
    const float contrast = 1.2;  // Contrast enhancement factor
    const int brightness = 10;   // Brightness adjustment
    
    // Apply contrast
    float adjusted = ((value / 255.0f - 0.5f) * contrast + 0.5f) * 255.0f;
    
    // Apply brightness
    adjusted += brightness;
    
    // Ensure value stays in valid range
    return (uint8_t)constrain(adjusted, 0, 255);
}

// Helper function to detect edges in the image
bool isEdge(uint8_t* buffer, int pos, int width) {
    // Check if current pixel is significantly different from neighbors
    uint8_t current = buffer[pos * 4 + 3];  // Alpha channel
    uint8_t left = (pos > 0) ? buffer[(pos-1) * 4 + 3] : 0;
    uint8_t right = (pos < width-1) ? buffer[(pos+1) * 4 + 3] : 0;
    
    return (abs(current - left) > 127 || abs(current - right) > 127);
}

// Helper function to smooth edges
void smoothEdge(Adafruit_ST7735& tft, int16_t x, int16_t y, uint16_t color) {
    // Get RGB components
    uint8_t r = (color >> 11) << 3;
    uint8_t g = ((color >> 5) & 0x3F) << 2;
    uint8_t b = (color & 0x1F) << 3;
    
    // Create slightly darker version for edge smoothing
    uint16_t edgeColor = safeColor565(
        r * 0.8,
        g * 0.8,
        b * 0.8
    );
    
    // Apply edge smoothing only if needed
    if ((x + y) % 2 == 0) {
        tft.drawPixel(x, y, edgeColor);
    }
}

void drawMenuIcon(const char* iconPath, int y) {
    int xCenter = BORDER_START_X + (BORDER_WIDTH - ICON_WIDTH) / 2;
    y = BORDER_START_Y + y;  // Hapus referensi ke MARGIN_TOP
    
    if (LittleFS.exists(iconPath)) {
        drawBMP(iconPath, xCenter, y, ICON_WIDTH, ICON_HEIGHT);
    } else {
        Serial.printf("Icon not found: %s\n", iconPath);
    }
}

float getBatteryVoltage() {
    if (!ina219Available) return 0.0;
    
    float busVoltage = ina219.getBusVoltage_V();
    float shuntVoltage = ina219.getShuntVoltage_mV() / 1000.0;
    float voltage = busVoltage + shuntVoltage;
    
    return (voltage < 2.0 || voltage > 5.0) ? 0.0 : voltage;
}

int getBatteryPercentage(float voltage) {
    float percentage = ((voltage - BATTERY_MIN) / (BATTERY_MAX - BATTERY_MIN)) * 100;
    return round(constrain(percentage, 0, 100));
}

void updateBatteryStatus() {
    float voltage = getBatteryVoltage();
    String currentVoltageStr = String(voltage, 2);
    
    if (currentVoltageStr != lastVoltageStr) {
        lastVoltageStr = currentVoltageStr;
        int percentage = getBatteryPercentage(voltage);
        
        if (percentage <= 3) {
            batteryStatus = "0%";
        } else if (percentage <= 20) {
            batteryStatus = "20%";
        } else if (percentage <= 50) {
            batteryStatus = "50%";
        } else {
            batteryStatus = "100%";
        }
        
        if (batteryStatus != prevBatteryStatus) {
            displayBatteryStatus();
        }
    }
}

void displayBatteryStatus() {
    // Adjust battery position relative to border
    const int battX = BORDER_START_X + BORDER_WIDTH - BATT_IMG_WIDTH - CONTENT_MARGIN;
    const int battY = BORDER_START_Y + CONTENT_MARGIN;
    
    if (batteryStatus != prevBatteryStatus) {
        tft.fillRect(battX, battY, BATT_IMG_WIDTH, BATT_IMG_HEIGHT, backgroundColor);
        
        const char* batteryImage = batteryStatus == "100%" ? "/battery_100.bmp" :
                          batteryStatus == "50%" ? "/battery_50.bmp" :
                          batteryStatus == "20%" ? "/battery_20.bmp" : "/battery_0.bmp";
        
        drawBMP(batteryImage, battX, battY, BATT_IMG_WIDTH, BATT_IMG_HEIGHT);
        prevBatteryStatus = batteryStatus;
    }
}

void drawText(const String& text, int x, int y, uint16_t textColor, int paddingX = 5) {
    int16_t x1, y1;
    uint16_t w, h;
    
    tft.getTextBounds(text, x, y, &x1, &y1, &w, &h);
    int actualX = x;
    tft.fillRect(actualX - paddingX, y1, w + (paddingX * 2), h, backgroundColor);
    tft.setTextColor(textColor);
    tft.setCursor(actualX, y);
    tft.print(text);
}

void drawCenteredText(const String& text, int y, uint16_t textColor, int paddingX = 5, int offsetX = 0) {
    int16_t x1, y1;
    uint16_t w, h;
    
    tft.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
    int availableWidth = BORDER_WIDTH - (CONTENT_MARGIN * 2);
    int x = BORDER_START_X + CONTENT_MARGIN + (availableWidth - w) / 2 + offsetX;
    y = BORDER_START_Y + y;  // Hapus referensi ke MARGIN_TOP
    
    drawText(text, x, y, textColor, paddingX);
}

void drawDottedLine(int y, int length, int dotSpacing, uint16_t color) {
    int borderStartX = (tft.width() - 80) / 2;
    int xStart = borderStartX + (80 - length) / 2;
    int xEnd = xStart + length;
    y = adjustContentY(y);
    
    for (int x = xStart; x <= xEnd; x += dotSpacing) {
        tft.drawPixel(x, y, color);
    }
}

void drawSmallIcon(const char* iconPath, int x, int y) {
    // Debug info
    Serial.printf("Drawing icon at x:%d y:%d with size %dx%d\n", 
                  x, y, SMALL_ICON_WIDTH, SMALL_ICON_HEIGHT);
    
    // Constrain position within border bounds
    x = constrain(x, BORDER_START_X + CONTENT_MARGIN, 
                 BORDER_START_X + BORDER_WIDTH - SMALL_ICON_WIDTH - CONTENT_MARGIN);
    y = constrain(y, BORDER_START_Y + CONTENT_MARGIN, 
                 BORDER_START_Y + BORDER_HEIGHT - SMALL_ICON_HEIGHT - CONTENT_MARGIN);
    
    if (LittleFS.exists(iconPath)) {
        drawBMP(iconPath, x, y, SMALL_ICON_WIDTH, SMALL_ICON_HEIGHT);
    } else {
        Serial.printf("Small icon not found: %s\n", iconPath);
    }
}

void clearTextArea(int x, int y, const String& text) {
    int16_t x1, y1;
    uint16_t w, h;
    
    tft.getTextBounds(text, x, y, &x1, &y1, &w, &h);
    tft.fillRect(x, y1, w + 2, h + 2, backgroundColor);
}

void updateTimeAndDate() {
    String hourStr = (hour() < 10 ? "0" : "") + String(hour());
    String minStr = (minute() < 10 ? "0" : "") + String(minute());
    timeStr = hourStr + ":" + minStr;

    String dayStr = (day() < 10 ? "0" : "") + String(day());
    String monthStr = (month() < 10 ? "0" : "") + String(month());
    String yearStr = String(year());
    dateStr = dayStr + " / " + monthStr + " / " + yearStr;
}

void displayTimeAndDate() {
    // Adjust base position relative to border
    int yBase = BORDER_START_Y + 70;
    int yStep = 10;
    
    // Display time with direct centering
    tft.setFont(&Open_24_Display_St17pt7b);
    if (timeStr != prevTimeStr) {
        int16_t x1, y1;
        uint16_t w, h;
        tft.getTextBounds(timeStr, 0, 0, &x1, &y1, &w, &h);
        
        // Calculate center position for time
        int x = BORDER_START_X + (BORDER_WIDTH - w) / 2;
        int y = yBase + yStep + 0;
        
        // Clear previous time area
        tft.fillRect(x + x1, y + y1, w, h, backgroundColor);
        
        // Draw centered time
        tft.setTextColor(safeColor565(255, 255, 255));
        tft.setCursor(x, y);
        tft.print(timeStr);
        
        prevTimeStr = timeStr;
    }
    
    // Display date with direct centering
    tft.setFont(&Swansea5pt7b);
    if (dateStr != prevDateStr) {
        int16_t x1, y1;
        uint16_t w, h;
        tft.getTextBounds(dateStr, 0, 0, &x1, &y1, &w, &h);
        
        // Calculate center position for date
        int x = BORDER_START_X + (BORDER_WIDTH - w) / 2;
        int y = yBase + yStep + 15;
        
        // Clear previous date area
        tft.fillRect(x + x1, y + y1, w, h, backgroundColor);
        
        // Draw centered date
        tft.setTextColor(safeColor565(221, 125, 64));
        tft.setCursor(x, y);
        tft.print(dateStr);
        
        prevDateStr = dateStr;
    }

    if (currentMenu == 1) {
        tft.setFont(&Swansea6pt7b);
        
        // Calculate proper icon positions
        int iconOffset = SMALL_ICON_HEIGHT / 2;
        
        int tempIconY = yBase + yStep + 30;
        int altIconY = yBase + yStep + 70;
        
        if (!iconsDrawnTimeMenu) {
            // Draw icons with adjusted positions and centering
            drawSmallIcon(ICON_TEMP, 
                        BORDER_START_X + CONTENT_MARGIN, 
                        tempIconY - iconOffset);
                        
            drawSmallIcon(ICON_ALTITUDE, 
                        BORDER_START_X + CONTENT_MARGIN, 
                        altIconY - iconOffset);
                        
            iconsDrawnTimeMenu = true;
        }

        // Update temperature value
        if (millis() - tempMillis >= tempInterval) {
            tempMillis = millis();
            
            if (bmpAvailable && bmpEnabled) {
                temperature = readFilteredTemperature();
                tempC = String(temperature, 1) + " *C";
            }
        }
            
        if (tempC != prevTempC) {
            int16_t x1, y1;
            uint16_t w, h;
            tft.getTextBounds(tempC, 0, 0, &x1, &y1, &w, &h);
            
            int textX = BORDER_START_X + BORDER_WIDTH - w - CONTENT_MARGIN;
            int textY = yBase + yStep + 38;
            
            tft.fillRect(textX, textY - h + y1, w, h, backgroundColor);
            tft.setTextColor(safeColor565(255, 255, 255));
            tft.setCursor(textX, textY);
            tft.print(tempC);
            prevTempC = tempC;
        }

        // Update altitude value
        if (millis() - altitudeMillis >= altitudeInterval) {
            altitudeMillis = millis();
            
            if (bmpAvailable && bmpEnabled) {
                altitude = bmp.readAltitude(1013.25);
                altitudeStr = String(altitude, 1) + " m";
            }
        }
            
        if (altitudeStr != prevAltitudeStr) {
            int16_t x1, y1;
            uint16_t w, h;
            tft.getTextBounds(altitudeStr, 0, 0, &x1, &y1, &w, &h);
            
            int textX = BORDER_START_X + BORDER_WIDTH - w - CONTENT_MARGIN;
            int textY = yBase + yStep + 63;
            
            tft.fillRect(textX, textY - h + y1, w, h, backgroundColor);
            tft.setTextColor(safeColor565(24, 218, 61));
            tft.setCursor(textX, textY);
            tft.print(altitudeStr);
            prevAltitudeStr = altitudeStr;
        }
    }
}

float calculateAngle(float ax, float ay, float az) {
    // Ensure we don't divide by zero
    if (ax == 0 && ay == 0 && az == 0) return 0;
    
    // Calculate angles using arctan2 for better quadrant handling
    // For X (Roll) - rotation around Y axis
    float roll = atan2(ay, az) * 180.0 / M_PI;
    
    // For Y (Pitch) - rotation around X axis
    float pitch = atan2(-ax, sqrt(ay * ay + az * az)) * 180.0 / M_PI;
    
    // For Z (Yaw) - can't be accurately determined with just accelerometer
    // Would need magnetometer for true heading
    float yaw = atan2(ax, ay) * 180.0 / M_PI;
    
    return roll; // Return roll by default, modify based on which angle you're calculating
}

float calculateMovingAverage() {
    float sum = 0;
    for (int i = 0; i < WINDOW_SIZE; i++) {
        sum += accelWindow[i];
    }
    return sum / WINDOW_SIZE;
}

float calculateMovingVariance(float average) {
    float variance = 0;
    for (int i = 0; i < WINDOW_SIZE; i++) {
        variance += pow(accelWindow[i] - average, 2);
    }
    return variance / WINDOW_SIZE;
}

void IRAM_ATTR ISR_downButton() {
    static unsigned long lastInterruptTime = 0;
    unsigned long interruptTime = millis();
    
    if (digitalRead(downButton) == LOW) {
        if (interruptTime - lastInterruptTime > DEBOUNCE_TIME) {
            buttonPressStartTime = interruptTime;
            isButtonDown = true;
            longPressExecuted = false;
        }
    } else {
        if (isButtonDown) {
            unsigned long pressDuration = interruptTime - buttonPressStartTime;
            
            if (pressDuration >= LONG_PRESS_DURATION && currentMenu == 6) {
                stepCount = 0;
                prevStepCount = "";
                longPressExecuted = true;
            } else if (pressDuration < LONG_PRESS_DURATION && !longPressExecuted) {
                buttonPressed = true;
            }
            isButtonDown = false;
        }
    }
    lastInterruptTime = interruptTime;
}

void setup() {
    Serial.begin(115200);
    Serial.println("Booting...");
    
    ina219Available = ina219.begin();
    if (!ina219Available) Serial.println("INA219 not found! Battery monitoring disabled");
    
    if (!LittleFS.begin()) {
        Serial.println("LittleFS initialization failed!");
        return;
    }

    Dir dir = LittleFS.openDir("/");
    Serial.println("Files in LittleFS:");
    while (dir.next()) {
        Serial.printf(" - %s (%d bytes)\n", dir.fileName().c_str(), dir.fileSize());
    }

    pinMode(downButton, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(downButton), ISR_downButton, CHANGE);

    tft.initR(INITR_GREENTAB);
    tft.setSPISpeed(40000000);
    delay(10);
    tft.invertDisplay(isInverted);
    setBackgroundColor(0, 0, 0);
    clearScreen();

    bmpAvailable = bmp.begin(0x76);
    if (!bmpAvailable) Serial.println("BMP280 not found! Values set to 0");

    mpuAvailable = mpu.begin();
    if (!mpuAvailable) Serial.println("MPU6050 not found! Values set to 0");

    if (bmpAvailable) {
        disableBMP280();
    }

    if (mpuAvailable) {
        disableMPU6050();
    }

    setTime(0, 0, 0, 1, 1, 2000);
    lastButtonPress = millis();
    
    drawBorder();
    updateBatteryStatus();
    displayBatteryStatus();
}

void loop() {
    updateBatteryStatus();
    
    if (wifiInitialized) {
        dnsServer.processNextRequest();
        server.handleClient();
    }
    
    if (autoReturnEnabled && (millis() - lastButtonPress >= AUTO_RETURN_TIMEOUT)) {
        currentMenu = 1;
        clearBorderArea();
        prevTimeStr = "";
        prevDateStr = "";
        prevTempC = "";
        prevTempF = "";
        prevPressureStr = "";
        prevAltitudeStr = "";
        prevAccX = "";
        prevAccY = "";
        prevAccZ = "";
        prevSettingsText = "";
        drawBorder();
        lastButtonPress = millis();
        
        disableMPU6050();
        enableBMP280();  // Menu 1 needs BMP280 for temperature display
    }

    if (buttonPressed) {
        buttonPressed = false;
        lastButtonPress = millis();
        resetKalmanFilters();
        
        int nextMenu;
        if (currentMenu == 1) {
            nextMenu = 2;  // Dari homescreen ke menu 2
        } else {
            nextMenu = (currentMenu % 7) + 1;
            if (nextMenu == 1) {  // Skip menu 1 dalam rotasi
                nextMenu = 2;
            }
        }
        
        if (nextMenu == 6 && currentMenu != 6) {
            stepCount = 0;
            prevStepCount = "";
            isFirstRun = true;
        }
        
        disableBMP280();
        disableMPU6050();
        
        switch (nextMenu) {
            case 1:
                enableBMP280();
                break;
            case 2:
                enableBMP280();
                break;
            case 3:
                enableBMP280();
                break;
            case 4:
                enableBMP280();
                break;
            case 5:
                enableMPU6050();
                break;
            case 6:
                enableMPU6050();
                break;
        }
        
        currentMenu = nextMenu;
        clearBorderArea();

        prevTimeStr = "";
        prevDateStr = "";
        prevTempC = "";
        prevTempF = "";
        prevPressureStr = "";
        prevAltitudeStr = "";
        prevAccX = "";
        prevAccY = "";
        prevAccZ = "";
        prevSettingsText = "";
        iconsDrawnTimeMenu = false;
        
        if (currentMenu == 2) {
            tempMillis = 0;
        }

        if (currentMenu == 1) {
            tempMillis = 0;
            altitudeMillis = 0;
        }
        
        if (currentMenu != 6 && wifiInitialized) {
            WiFi.softAPdisconnect(true);
            server.close();
            wifiInitialized = false;
            Serial.println("WiFi AP stopped");
        }
        
        drawBorder();
    }

    updateTimeAndDate();

    switch (currentMenu) {
        case 1: 
            autoReturnEnabled = false;
            displayTimeAndDate();
            break;

       case 2: {
            autoReturnEnabled = true;
            tft.setFont(&Swansea6pt7b);
            drawMenuIcon(ICON_TEMP, CONTENT_MARGIN + 20);
            
            if (millis() - tempMillis >= tempInterval) {
                tempMillis = millis();
                
                if (bmpAvailable && bmpEnabled) {
                    temperature = readFilteredTemperature();
                    tempC = String(temperature, 1) + " *C";
                    tempF = String((temperature * 9/5) + 32, 1) + " *F";
                }
            }
                
            int yBase = CONTENT_MARGIN + ICON_HEIGHT + 0;
            int yStep = 30;
            
            if (tempC != prevTempC) {
                drawCenteredText(tempC, yBase + yStep + 0, safeColor565(255, 255, 255));
                prevTempC = tempC;
            }
            
            drawDottedLine(yBase + yStep + 7, 55, 4, safeColor565(255, 255, 255));
            
            if (tempF != prevTempF) {
                drawCenteredText(tempF, yBase + yStep + 22, safeColor565(221, 125, 64));
                prevTempF = tempF;
            }
        }
        break;

        case 3: {
            autoReturnEnabled = true;
            tft.setFont(&Swansea6pt7b);
            drawMenuIcon(ICON_PRESSURE, CONTENT_MARGIN + 20);
            
            if (millis() - pressureMillis >= pressureInterval) {
                pressureMillis = millis();
                
                if (bmpAvailable && bmpEnabled) {
                    pressure = readFilteredPressure();
                    lastPressure = pressure;
                    pressureStr = String(pressure, 1) + " hPa";
                }
            }
                
            int yBase = CONTENT_MARGIN + ICON_HEIGHT + 0;
            int yStep = 30;
            
            if (pressureStr != prevPressureStr) {
                drawCenteredText(pressureStr, yBase + yStep + 0, safeColor565(255, 100, 100));
                prevPressureStr = pressureStr;
            }
        }
        break;

        case 4: {
            autoReturnEnabled = true;
            tft.setFont(&Swansea6pt7b);
            drawMenuIcon(ICON_ALTITUDE, CONTENT_MARGIN + 20);
            
            if (millis() - altitudeMillis >= altitudeInterval) {
                altitudeMillis = millis();
                
                if (bmpAvailable && bmpEnabled) {
                    altitude = readFilteredAltitude();
                    altitudeStr = String(altitude, 1) + " m";
                }
            }
                
            int yBase = CONTENT_MARGIN + ICON_HEIGHT + 0;
            int yStep = 30;
            
            if (altitudeStr != prevAltitudeStr) {
                drawCenteredText(altitudeStr, yBase + yStep + 0, safeColor565(24, 218, 61));
                prevAltitudeStr = altitudeStr;
            }
        }
        break;

        case 5: {
            autoReturnEnabled = true;
            tft.setFont(&Swansea6pt7b);
            drawMenuIcon(ICON_ANGLE, CONTENT_MARGIN + 20);
            
            if (millis() - sensorMillis >= sensorInterval) {
                sensorMillis = millis();
                
                if (mpuAvailable && mpuEnabled) {
                    FilteredMPUData filtered = readFilteredAcceleration();
                    
                    // Update display strings with angular data
                    accX = "X: " + String(filtered.roll, 1) + "*";   // Left-Right tilt
                    accY = "Y: " + String(filtered.pitch, 1) + "*";  // Forward-Backward tilt
                    /*
                      accZ = "Z: " + String(filtered.yaw, 1) + "*";    // Rotation
                    */
                }
            }
                
            int yBase = CONTENT_MARGIN + ICON_HEIGHT + 0;
            int yStep = 30;
            
            if (accX != prevAccX) {
                drawCenteredText(accX, yBase + yStep + 0, safeColor565(221, 67, 196));
                prevAccX = accX;
            }
            if (accY != prevAccY) {
                drawCenteredText(accY, yBase + yStep + 15, safeColor565(101, 111, 188));
                prevAccY = accY;
            }
            /*
              if (accZ != prevAccZ) {
                  drawCenteredText(accZ, yBase + yStep + 30, safeColor565(249, 218, 188));
                  prevAccZ = accZ;
              }
            */
          }
          break;

        case 6: {
            autoReturnEnabled = false;
            static const unsigned long MAX_STEPS = 99999999;
            static KalmanFilter accelMagKalman(0.01, 0.1); // Process noise, measurement noise
            static bool isKalmanInitialized = false;
            
            if (isButtonDown && (millis() - buttonPressStartTime) >= LONG_PRESS_DURATION && currentMenu == 6) {
                stepCount = 0;
                prevStepCount = "";
                isButtonDown = false;
                buttonPressed = false;
                isKalmanInitialized = false;
                accelMagKalman.reset();
            }
            
            if (isFirstRun) {
                for (int i = 0; i < WINDOW_SIZE; i++) {
                    accelWindow[i] = 0;
                }
                isFirstRun = false;
                isKalmanInitialized = false;
                accelMagKalman.reset();
            }
            
            tft.setFont(&Swansea6pt7b);
            drawMenuIcon(ICON_STEP, CONTENT_MARGIN + 20);
            
            if (mpuAvailable && mpuEnabled) {
                sensors_event_t a, g, temp;
                mpu.getEvent(&a, &g, &temp);
                
                // Calculate acceleration magnitude with gravity compensation
                float rawMagnitude = sqrt(
                    a.acceleration.x * a.acceleration.x + 
                    a.acceleration.y * a.acceleration.y + 
                    (a.acceleration.z - 9.81) * (a.acceleration.z - 9.81)
                );
                
                // Apply Kalman filter to smooth the magnitude
                float filteredMagnitude = accelMagKalman.update(rawMagnitude);
                
                // Noise threshold with hysteresis
                const float NOISE_THRESHOLD_HIGH = 0.9;
                const float NOISE_THRESHOLD_LOW = 0.7;
                static bool isAboveNoise = false;
                
                if (filteredMagnitude > NOISE_THRESHOLD_HIGH) {
                    isAboveNoise = true;
                } else if (filteredMagnitude < NOISE_THRESHOLD_LOW) {
                    isAboveNoise = false;
                }
                
                if (isAboveNoise) {
                    accelWindow[windowIndex] = filteredMagnitude;
                } else {
                    accelWindow[windowIndex] = 0;
                }
                windowIndex = (windowIndex + 1) % WINDOW_SIZE;
                
                float movingAvg = calculateMovingAverage();
                float movingVar = calculateMovingVariance(movingAvg);
                
                static bool isPeak = false;
                static float peakValue = 0;
                static float valleyValue = 0;

                // Peak detection with enhanced conditions
                if (filteredMagnitude > movingAvg && !isPeak && 
                    movingVar > VARIANCE_THRESHOLD && 
                    (millis() - lastStepTime) > MIN_STEP_INTERVAL &&
                    isAboveNoise) {
                    
                    isPeak = true;
                    peakValue = filteredMagnitude;
                } 
                else if (filteredMagnitude < movingAvg && isPeak) {
                    valleyValue = filteredMagnitude;
                    
                    float stepMagnitude = peakValue - valleyValue;
                    
                    // Enhanced step validation
                    if (stepMagnitude > STEP_MAGNITUDE_THRESHOLD && 
                        peakValue > PEAK_THRESHOLD) {
                        
                        // Additional validation using variance
                        if (movingVar > VARIANCE_THRESHOLD * 1.2) {
                            stepCount++;
                            if (stepCount >= MAX_STEPS) {
                                stepCount = 0;
                            }
                            lastStepTime = millis();
                        }
                    }
                    isPeak = false;
                }
            }
            
            String currentStepCount = String(stepCount);
            int yBase = CONTENT_MARGIN + ICON_HEIGHT + 0;
            int yStep = 30;
            
            if (currentStepCount != prevStepCount) {
                drawCenteredText(currentStepCount, yBase + yStep + 0, safeColor565(255, 255, 255));
                prevStepCount = currentStepCount;
            }
        }
        break;

        case 7: {
            autoReturnEnabled = false;
            if (!wifiInitialized) {
                WiFi.mode(WIFI_AP);
                WiFi.softAP(ssid, password);
                setupWebServer();
                wifiInitialized = true;
                Serial.println("Access Point Started");
                Serial.print("IP Address: ");
                Serial.println(WiFi.softAPIP());
            }
            
            tft.setFont(&Swansea6pt7b);
            drawMenuIcon(ICON_SETTINGS, CONTENT_MARGIN + 25);
            
            if (settingsText != prevSettingsText) {
                int yBase = CONTENT_MARGIN + ICON_HEIGHT + 0;
                int yStep = 30;
                drawCenteredText(settingsText, yBase + yStep + 0, safeColor565(255, 255, 255));
                prevSettingsText = settingsText;
            }
        }
        break;
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant