Open
Description
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;
}
}
Metadata
Metadata
Assignees
Labels
No labels