The IR sensor never triggers the time, what's wrong? When the start sensor is over time, checkStartSensor and checkFinishSensor should run according to their function, but this doesn't work at all.
/*
* ESP32-2432S024 Sistem Timer Balap
* LVGL 9.2.0 + FreeRTOS + Sensor IR Break Beam NC
* Timer Balap Multi Peserta dengan Riwayat & Ekspor
*
* FIXED v2.3: Deteksi HIGHβLOW untuk sensor NC
* Mode: Bisa test dengan 1 sensor START saja
*/
#include "ArduinoJson.h"
#include "WiFi.h"
#include "lvgl.h"
#include "TFT_eSPI.h"
#include "XPT2046_Touchscreen.h"
#include "SPI.h"
#include "LittleFS.h"
#include "ESPAsyncWebServer.h"
using namespace fs;
#include "src/ui.h"
#include "src/screens.h"
#include "src/images.h"
#include "src/fonts.h"
// ================================
// DEFINISI PIN
// ================================
#define TFT_BL 27
#define TOUCH_CS 33
#define TOUCH_IRQ 36
#define TOUCH_MOSI 13
#define TOUCH_MISO 12
#define TOUCH_CLK 14
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
#define IR_START_PIN 21
#define IR_FINISH_PIN 22
#define TS_MIN_X 370
#define TS_MAX_X 3700
#define TS_MIN_Y 470
#define TS_MAX_Y 3600
#define TFT_BL_CHANNEL 0
#define TFT_BL_FREQ 5000
#define TFT_BL_RESOLUTION 8
#define TFT_BL_BRIGHTNESS 180
#define MAX_HISTORY 10
#define HISTORY_FILE "/race_history.json"
// ================================
// OBJEK GLOBAL
// ================================
static lv_display_t *display;
static lv_indev_t *indev;
static uint8_t buf[SCREEN_WIDTH * 10];
TFT_eSPI tft = TFT_eSPI();
SPIClass touchSPI = SPIClass(VSPI);
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);
AsyncWebServer server(80);
// ================================
// SEMAPHORE
// ================================
SemaphoreHandle_t displayMutex;
SemaphoreHandle_t raceMutex;
portMUX_TYPE raceMux = portMUX_INITIALIZER_UNLOCKED;
SemaphoreHandle_t spiMutex;
// ================================
// STATUS BALAPAN
// ================================
struct RacingState {
int maxParticipants;
bool isRunning;
bool waitingStart;
unsigned long raceStartTime;
unsigned long raceDuration;
int finishedCount;
unsigned long finishTimes[10];
bool canReset;
int currentLap;
int maxLaps;
unsigned long lapTimes[10][10];
bool lapMode;
};
RacingState raceState = {
.maxParticipants = 1,
.isRunning = false,
.waitingStart = false,
.raceStartTime = 0,
.raceDuration = 0,
.finishedCount = 0,
.canReset = false,
.currentLap = 0,
.maxLaps = 1,
.lapMode = false
};
// ================================
// DETEKSI FINISH
// ================================
struct FinishDetection {
unsigned long lastTriggerTime;
int pendingFinishes;
unsigned long triggerBuffer[10];
int bufferCount;
};
FinishDetection finishDetector = {
.lastTriggerTime = 0,
.pendingFinishes = 0,
.bufferCount = 0
};
const unsigned long MULTI_FINISH_WINDOW = 100;
// PENTING: Untuk sensor NC, state awal tergantung apakah sensor terhalang atau tidak
// HIGH = terhalang/blocked, LOW = clear/tidak terhalang
bool lastStartState = LOW;
bool lastFinishState = LOW;
unsigned long lastStartDebounce = 0;
unsigned long lastFinishDebounce = 0;
const unsigned long DEBOUNCE_DELAY = 20;
// ================================
// STRUKTUR RIWAYAT BALAPAN
// ================================
struct RaceRecord {
unsigned long timestamp;
int participants;
int laps;
unsigned long times[10];
String date;
String time;
};
std::vector<RaceRecord> raceHistory;
// ================================
// KONFIGURASI WIFI
// ================================
String AP_SSID = "RacingTimer-" + String(ESP.getEfuseMac(), HEX);
String AP_PASSWORD = "racing123";
bool touchPressed = false;
int16_t lastX = 0;
int16_t lastY = 0;
TaskHandle_t uiTaskHandle = NULL;
TaskHandle_t sensorTaskHandle = NULL;
TaskHandle_t timerTaskHandle = NULL;
volatile bool needUpdateStartLabel = false;
// ================================
// DEKLARASI FUNGSI
// ================================
void updateTimeDisplay();
void updateParticipantDisplay();
void updateStartLabel();
void clearAllTimes();
void handleStartButton();
void handleResetButton();
void checkStartSensor();
void checkFinishSensor();
void saveParticipantConfig();
void loadParticipantConfig();
void setupWebServer();
void saveRaceToHistory();
void loadRaceHistory();
void saveHistoryToFile();
String getCurrentDateTime();
void processFinishBuffer();
// ================================
// FORMAT WAKTU
// ================================
String formatTime(unsigned long ms) {
unsigned long totalSeconds = ms / 1000;
unsigned long minutes = totalSeconds / 60;
unsigned long seconds = totalSeconds % 60;
unsigned long millis = (ms % 1000) / 10;
char buffer[12];
sprintf(buffer, "%02lu:%02lu.%02lu", minutes, seconds, millis);
return String(buffer);
}
String getCurrentDateTime() {
unsigned long currentMillis = millis();
unsigned long totalSeconds = currentMillis / 1000;
unsigned long hours = (totalSeconds / 3600) % 24;
unsigned long minutes = (totalSeconds / 60) % 60;
unsigned long seconds = totalSeconds % 60;
char buffer[20];
sprintf(buffer, "%02lu:%02lu:%02lu", hours, minutes, seconds);
return String(buffer);
}
// ================================
// MANAJEMEN RIWAYAT BALAPAN
// ================================
void saveHistoryToFile() {
JsonDocument doc;
JsonArray races = doc["races"].to<JsonArray>();
for (const auto& record : raceHistory) {
JsonObject race = races.add<JsonObject>();
race["timestamp"] = record.timestamp;
race["participants"] = record.participants;
race["laps"] = record.laps;
race["time"] = record.time;
JsonArray times = race["times"].to<JsonArray>();
for (int i = 0; i < record.participants; i++) {
times.add(record.times[i]);
}
}
File file = LittleFS.open(HISTORY_FILE, "w");
if (file) {
serializeJson(doc, file);
file.close();
Serial.println("Riwayat disimpan ke file");
} else {
Serial.println("Gagal menyimpan file riwayat");
}
}
void loadRaceHistory() {
if (!LittleFS.exists(HISTORY_FILE)) {
Serial.println("File riwayat tidak ditemukan");
return;
}
File file = LittleFS.open(HISTORY_FILE, "r");
if (!file) {
Serial.println("Gagal membuka file riwayat");
return;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) {
Serial.println("Gagal parsing file riwayat: " + String(error.c_str()));
return;
}
raceHistory.clear();
JsonArray races = doc["races"];
for (JsonObject race : races) {
RaceRecord record;
record.timestamp = race["timestamp"] | 0UL;
record.participants = race["participants"] | 1;
record.laps = race["laps"] | 1;
record.time = race["time"].as<String>();
JsonArray times = race["times"];
int i = 0;
for (JsonVariant v : times) {
if (i < 10) {
record.times[i] = v.as<unsigned long>();
i++;
}
}
raceHistory.push_back(record);
}
Serial.println("Berhasil memuat " + String(raceHistory.size()) + " riwayat balapan");
}
void saveRaceToHistory() {
if (raceState.finishedCount == 0) return;
RaceRecord record;
record.timestamp = millis();
record.participants = raceState.maxParticipants;
record.laps = raceState.maxLaps;
record.time = getCurrentDateTime();
for (int i = 0; i < raceState.maxParticipants; i++) {
record.times[i] = raceState.finishTimes[i];
}
raceHistory.insert(raceHistory.begin(), record);
if (raceHistory.size() > MAX_HISTORY) {
raceHistory.pop_back();
}
saveHistoryToFile();
Serial.println("Balapan disimpan ke riwayat");
}
// ================================
// SIMPAN/MUAT KONFIGURASI
// ================================
void saveParticipantConfig() {
JsonDocument doc;
doc["participants"] = raceState.maxParticipants;
doc["lapMode"] = raceState.lapMode;
doc["maxLaps"] = raceState.maxLaps;
File file = LittleFS.open("/racing_config.json", "w");
if (file) {
serializeJson(doc, file);
file.close();
Serial.println("Konfigurasi disimpan: " + String(raceState.maxParticipants) + " peserta, " +
String(raceState.maxLaps) + " lap");
} else {
Serial.println("Gagal menyimpan konfigurasi");
}
}
void loadParticipantConfig() {
if (!LittleFS.exists("/racing_config.json")) {
Serial.println("File konfigurasi tidak ada, gunakan default");
return;
}
File file = LittleFS.open("/racing_config.json", "r");
if (!file) {
Serial.println("Gagal membuka file konfigurasi");
return;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, file);
file.close();
if (error) {
Serial.println("Gagal parsing konfigurasi: " + String(error.c_str()));
return;
}
raceState.maxParticipants = doc["participants"] | 1;
raceState.lapMode = doc["lapMode"] | false;
raceState.maxLaps = doc["maxLaps"] | 1;
Serial.println("Konfigurasi dimuat: " + String(raceState.maxParticipants) + " peserta, " +
String(raceState.maxLaps) + " lap");
}
// ================================
// UPDATE TAMPILAN
// ================================
void updateStartLabel() {
Serial.println("[DEBUG] updateStartLabel() called");
if (objects.label_start) {
Serial.println("[DEBUG] objects.label_start is valid");
if (raceState.isRunning || raceState.waitingStart) {
lv_label_set_text(objects.label_start, "STOP");
Serial.println("[DEBUG] Label set ke STOP");
} else {
lv_label_set_text(objects.label_start, "START");
Serial.println("[DEBUG] Label set ke START");
}
lv_obj_invalidate(objects.label_start);
Serial.println("[DEBUG] Label invalidated");
} else {
Serial.println("[ERROR] objects.label_start is NULL!");
}
}
void updateTimeDisplay() {
if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
String timeStr;
if (raceState.isRunning) {
timeStr = formatTime(raceState.raceDuration);
} else if (raceState.raceDuration > 0) {
timeStr = formatTime(raceState.raceDuration);
} else {
timeStr = "00:00.00";
}
if (objects.time_run) {
lv_label_set_text(objects.time_run, timeStr.c_str());
}
xSemaphoreGive(displayMutex);
}
}
void updateParticipantDisplay() {
if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
lv_obj_t* timeLabels[] = {
objects.time_ridding1, objects.time_ridding2, objects.time_ridding3,
objects.time_ridding4, objects.time_ridding5, objects.time_ridding6,
objects.time_ridding7, objects.time_ridding8, objects.time_ridding9,
objects.time_ridding10
};
lv_obj_t* cupLabels[] = {
objects.cup_ridding1, objects.cup_ridding2, objects.cup_ridding3,
objects.cup_ridding4, objects.cup_ridding5, objects.cup_ridding6,
objects.cup_ridding7, objects.cup_ridding8, objects.cup_ridding9,
objects.cup_ridding10
};
for (int i = 0; i < 10; i++) {
if (i < raceState.maxParticipants) {
if (timeLabels[i]) {
lv_obj_clear_flag(timeLabels[i], LV_OBJ_FLAG_HIDDEN);
if (i < raceState.finishedCount && raceState.finishTimes[i] > 0) {
String timeStr = formatTime(raceState.finishTimes[i]);
if (raceState.lapMode) {
timeStr += " L" + String(raceState.maxLaps);
}
lv_label_set_text(timeLabels[i], timeStr.c_str());
} else {
lv_label_set_text(timeLabels[i], "00:00.00");
}
}
if (cupLabels[i]) {
lv_obj_clear_flag(cupLabels[i], LV_OBJ_FLAG_HIDDEN);
String cupStr = "P" + String(i + 1);
lv_label_set_text(cupLabels[i], cupStr.c_str());
}
} else {
if (timeLabels[i]) {
lv_obj_add_flag(timeLabels[i], LV_OBJ_FLAG_HIDDEN);
}
if (cupLabels[i]) {
lv_obj_add_flag(cupLabels[i], LV_OBJ_FLAG_HIDDEN);
}
}
}
xSemaphoreGive(displayMutex);
}
}
void clearAllTimes() {
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
raceState.raceDuration = 0;
raceState.finishedCount = 0;
raceState.isRunning = false;
raceState.waitingStart = false;
raceState.canReset = false;
raceState.currentLap = 0;
finishDetector.lastTriggerTime = 0;
finishDetector.pendingFinishes = 0;
finishDetector.bufferCount = 0;
for (int i = 0; i < 10; i++) {
raceState.finishTimes[i] = 0;
for (int j = 0; j < 10; j++) {
raceState.lapTimes[i][j] = 0;
}
}
xSemaphoreGive(raceMutex);
}
updateTimeDisplay();
updateParticipantDisplay();
updateStartLabel();
Serial.println("Semua waktu dihapus");
}
// ================================
// HANDLER TOMBOL
// ================================
void handleStartButton() {
Serial.println("[ACTION] handleStartButton called");
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
if (!raceState.isRunning && !raceState.waitingStart) {
Serial.println("[STATE] Starting race preparation");
raceState.waitingStart = true;
raceState.canReset = false;
needUpdateStartLabel = true;
Serial.println("[DEBUG] needUpdateStartLabel flag set to true");
xSemaphoreGive(raceMutex);
Serial.println("========================================");
Serial.println("RACE READY - Waiting for IR start");
Serial.println("Lewati sensor START (HIGHβLOW) untuk mulai");
Serial.println("Press STOP to cancel");
Serial.println("========================================");
Serial.println("Participants: " + String(raceState.maxParticipants));
if (raceState.lapMode) {
Serial.println("Mode: Lap Race (" + String(raceState.maxLaps) + " laps)");
} else {
Serial.println("Mode: Single Race");
}
Serial.println("========================================");
} else if (raceState.isRunning || raceState.waitingStart) {
Serial.println("[STATE] Stopping race");
raceState.isRunning = false;
raceState.waitingStart = false;
raceState.canReset = true;
needUpdateStartLabel = true;
Serial.println("[DEBUG] needUpdateStartLabel flag set to true");
xSemaphoreGive(raceMutex);
if (raceState.finishedCount > 0) {
saveRaceToHistory();
}
Serial.println("========================================");
Serial.println("RACE STOPPED");
Serial.println("========================================");
Serial.println("Final Time: " + formatTime(raceState.raceDuration));
Serial.println("Finished: " + String(raceState.finishedCount) + "/" + String(raceState.maxParticipants));
if (raceState.finishedCount > 0) {
Serial.println("Race saved to history");
}
Serial.println("========================================");
} else {
xSemaphoreGive(raceMutex);
}
} else {
Serial.println("[ERROR] Failed to acquire raceMutex");
}
}
void handleResetButton() {
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
if (raceState.canReset && !raceState.isRunning && !raceState.waitingStart) {
xSemaphoreGive(raceMutex);
Serial.println("========================================");
Serial.println("RESET DIAKTIFKAN");
Serial.println("========================================");
clearAllTimes();
Serial.println("Siap untuk balapan baru");
Serial.println("========================================");
} else {
xSemaphoreGive(raceMutex);
Serial.println("========================================");
Serial.println("RESET DIBLOKIR");
Serial.println("========================================");
if (raceState.isRunning || raceState.waitingStart) {
Serial.println("Alasan: Balapan masih berjalan atau disiapkan");
Serial.println("Aksi: Tekan tombol START untuk stop dulu");
}
Serial.println("========================================");
}
}
}
// ================================
// PEMROSES BUFFER FINISH
// ================================
void processFinishBuffer() {
if (finishDetector.bufferCount == 0) return;
Serial.println("========================================");
Serial.println("MEMPROSES BUFFER FINISH");
Serial.println("========================================");
Serial.println("Trigger terdeteksi: " + String(finishDetector.bufferCount));
int estimatedFinishers = finishDetector.bufferCount;
int remainingParticipants = raceState.maxParticipants - raceState.finishedCount;
if (estimatedFinishers > remainingParticipants) {
estimatedFinishers = remainingParticipants;
}
unsigned long avgTime = 0;
for (int i = 0; i < finishDetector.bufferCount; i++) {
avgTime += finishDetector.triggerBuffer[i];
}
avgTime = avgTime / finishDetector.bufferCount;
if (raceState.lapMode) {
raceState.lapTimes[0][raceState.currentLap - 1] = avgTime;
if (raceState.currentLap >= raceState.maxLaps) {
raceState.finishTimes[0] = avgTime;
raceState.finishedCount = 1;
Serial.println("Participant 1 finished all laps!");
Serial.println("Final time: " + formatTime(avgTime));
} else {
Serial.println("Lap " + String(raceState.currentLap) + " completed: " + formatTime(avgTime));
raceState.currentLap++;
}
} else {
for (int i = 0; i < estimatedFinishers; i++) {
if (raceState.finishedCount < raceState.maxParticipants) {
raceState.finishTimes[raceState.finishedCount] = avgTime;
raceState.finishedCount++;
Serial.println("Participant " + String(raceState.finishedCount) + " finished: " + formatTime(avgTime));
}
}
}
updateParticipantDisplay();
if (raceState.finishedCount >= raceState.maxParticipants) {
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
raceState.isRunning = false;
raceState.canReset = true;
xSemaphoreGive(raceMutex);
}
needUpdateStartLabel = true;
Serial.println("[DEBUG] needUpdateStartLabel flag set to true (all finished)");
saveRaceToHistory();
Serial.println("========================================");
Serial.println("SEMUA PESERTA SELESAI!");
Serial.println("========================================");
}
Serial.println("========================================");
finishDetector.bufferCount = 0;
finishDetector.pendingFinishes = 0;
}
// ================================
// PENANGANAN SENSOR IR - FIXED!
// ================================
// Sensor NC: HIGH = blocked/terhalang, LOW = clear/tidak terhalang
// Transisi yang dideteksi: HIGH β LOW (melewati sensor)
void checkStartSensor() {
bool currentState = digitalRead(IR_START_PIN);
// Debug sensor state setiap 1 detik
static unsigned long lastDebugPrint = 0;
if (millis() - lastDebugPrint > 1000) {
Serial.printf("[START_SENSOR] State: %s (waiting: %d, running: %d)\n",
currentState ? "HIGH(blocked)" : "LOW(clear)",
raceState.waitingStart, raceState.isRunning);
lastDebugPrint = millis();
}
if (currentState != lastStartState) {
lastStartDebounce = millis();
}
if ((millis() - lastStartDebounce) > DEBOUNCE_DELAY) {
// β
Deteksi HIGH β LOW (melewati sensor)
if (lastStartState == HIGH && currentState == LOW) {
Serial.println("βββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β [START_SENSOR] TRANSITION DETECTED! β");
Serial.println("β HIGH β LOW (sensor dilewati!) β");
Serial.println("βββββββββββββββββββββββββββββββββββββββββ");
// π₯ ULTIMATE FIX: Increase timeout jadi 200ms dan retry logic
bool acquired = false;
int retries = 0;
const int maxRetries = 3;
while (!acquired && retries < maxRetries) {
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(200)) == pdTRUE) {
acquired = true;
// Check kondisi DALAM mutex
if (raceState.waitingStart && !raceState.isRunning) {
Serial.println("[START_SENSOR] β
Mutex acquired, starting race!");
// π₯ CRITICAL SECTION - Set ALL states atomically
unsigned long currentTime = millis();
// Disable interrupts untuk memastikan atomic operation
portENTER_CRITICAL(&raceMux);
raceState.waitingStart = false;
raceState.isRunning = true; // π₯ KEY STATE
raceState.raceStartTime = currentTime;
raceState.raceDuration = 0;
raceState.finishedCount = 0;
raceState.currentLap = 1;
portEXIT_CRITICAL(&raceMux);
// Verify state
bool stateOK = (raceState.isRunning == true &&
raceState.waitingStart == false);
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β π RACE STARTED! π β");
Serial.println("β βββββββββββββββββββββββββββββββββββββββββ£");
Serial.printf("β isRunning: %d β
\n", raceState.isRunning);
Serial.printf("β waitingStart: %d β
\n", raceState.waitingStart);
Serial.printf("β startTime: %lu ms\n", currentTime);
Serial.printf("β Verified: %s\n", stateOK ? "YES β
" : "NO β");
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ");
needUpdateStartLabel = true;
// Release mutex
xSemaphoreGive(raceMutex);
// Triple-check setelah release
vTaskDelay(pdMS_TO_TICKS(10)); // Small delay
Serial.printf("[POST-CHECK] isRunning=%d (should be 1)\n",
raceState.isRunning);
if (!stateOK || raceState.isRunning != true) {
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β β CRITICAL ERROR β β");
Serial.println("β State verification FAILED! β");
Serial.println("β Race did NOT start properly! β");
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ");
}
} else {
// Kondisi tidak memenuhi
Serial.printf("[START_SENSOR] Ignored (waiting=%d, running=%d)\n",
raceState.waitingStart, raceState.isRunning);
xSemaphoreGive(raceMutex);
}
} else {
// Gagal acquire mutex, retry
retries++;
Serial.printf("[START_SENSOR] β οΈ Mutex acquire failed (retry %d/%d)\n",
retries, maxRetries);
vTaskDelay(pdMS_TO_TICKS(20)); // Small delay before retry
}
}
if (!acquired) {
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β β FATAL ERROR β β");
Serial.println("β Could not acquire mutex after 3 triesβ");
Serial.println("β System might be deadlocked! β");
Serial.println("β Try: Press RESET button on ESP32 β");
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ");
}
}
}
lastStartState = currentState;
}
// Sensor FINISH - bisa dikosongkan jika belum dipasang
void checkFinishSensor() {
bool currentState = digitalRead(IR_FINISH_PIN);
// Debug sensor state setiap 1 detik
static unsigned long lastDebugPrint = 0;
if (millis() - lastDebugPrint > 1000) {
Serial.printf("[FINISH_SENSOR] State: %s (running: %d, finished: %d/%d)\n",
currentState ? "HIGH(blocked)" : "LOW(clear)",
raceState.isRunning,
raceState.finishedCount,
raceState.maxParticipants);
lastDebugPrint = millis();
}
if (currentState != lastFinishState) {
lastFinishDebounce = millis();
}
if ((millis() - lastFinishDebounce) > DEBOUNCE_DELAY) {
// β
FIXED: Untuk sensor NC, deteksi HIGH β LOW (melewati sensor)
if (lastFinishState == HIGH && currentState == LOW) {
Serial.println("βββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β [FINISH_SENSOR] TRANSITION DETECTED! β");
Serial.println("β HIGH β LOW (sensor dilewati!) β");
Serial.println("βββββββββββββββββββββββββββββββββββββββββ");
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
if (raceState.isRunning && raceState.finishedCount < raceState.maxParticipants) {
unsigned long finishTime = millis() - raceState.raceStartTime;
unsigned long currentMillis = millis();
Serial.println("[FINISH_SENSOR] β
Valid finish detected!");
Serial.println("[FINISH_SENSOR] Time: " + formatTime(finishTime));
if (currentMillis - finishDetector.lastTriggerTime < MULTI_FINISH_WINDOW) {
finishDetector.triggerBuffer[finishDetector.bufferCount] = finishTime;
finishDetector.bufferCount++;
finishDetector.pendingFinishes++;
Serial.println("[FINISH_SENSOR] Added to buffer (count: " + String(finishDetector.bufferCount) + ")");
} else {
if (finishDetector.bufferCount > 0) {
processFinishBuffer();
}
finishDetector.triggerBuffer[0] = finishTime;
finishDetector.bufferCount = 1;
finishDetector.pendingFinishes = 1;
Serial.println("[FINISH_SENSOR] New buffer started");
}
finishDetector.lastTriggerTime = currentMillis;
xSemaphoreGive(raceMutex);
} else {
xSemaphoreGive(raceMutex);
if (!raceState.isRunning) {
Serial.println("[FINISH_SENSOR] β οΈ Ignored - race not running");
} else {
Serial.println("[FINISH_SENSOR] β οΈ Ignored - all participants finished");
}
}
}
}
}
lastFinishState = currentState;
// Process buffer if window expired
if (finishDetector.bufferCount > 0 &&
(millis() - finishDetector.lastTriggerTime) >= MULTI_FINISH_WINDOW) {
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
processFinishBuffer();
xSemaphoreGive(raceMutex);
}
}
}
// ================================
// WEB SERVER (singkat, sesuai kebutuhan)
// ================================
void setupWebServer() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
if (!LittleFS.exists("/index.html")) {
request->send(404, "text/plain", "index.html tidak ditemukan");
return;
}
request->send(LittleFS, "/index.html", "text/html");
});
server.on("/setparticipants", HTTP_GET, [](AsyncWebServerRequest *request) {
if (request->hasParam("count")) {
int count = request->getParam("count")->value().toInt();
if (count >= 1 && count <= 10) {
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
raceState.maxParticipants = count;
xSemaphoreGive(raceMutex);
saveParticipantConfig();
updateParticipantDisplay();
request->send(200, "text/plain", "OK");
Serial.println("Jumlah peserta diatur ke: " + String(count));
} else {
request->send(500, "text/plain", "Gagal mendapatkan lock");
}
} else {
request->send(400, "text/plain", "Count harus antara 1-10");
}
} else {
request->send(400, "text/plain", "Parameter count diperlukan");
}
});
server.on("/currentrace", HTTP_GET, [](AsyncWebServerRequest *request) {
JsonDocument doc;
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
doc["isRunning"] = raceState.isRunning;
doc["waitingStart"] = raceState.waitingStart;
doc["participants"] = raceState.maxParticipants;
doc["finishedCount"] = raceState.finishedCount;
doc["duration"] = raceState.raceDuration;
doc["lapMode"] = raceState.lapMode;
doc["currentLap"] = raceState.currentLap;
doc["maxLaps"] = raceState.maxLaps;
JsonArray times = doc["finishTimes"].to<JsonArray>();
for (int i = 0; i < raceState.finishedCount; i++) {
times.add(raceState.finishTimes[i]);
}
xSemaphoreGive(raceMutex);
}
String output;
serializeJson(doc, output);
request->send(200, "application/json", output);
});
server.on("/forcereset", HTTP_POST, [](AsyncWebServerRequest *request) {
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
raceState.isRunning = false;
raceState.waitingStart = false;
xSemaphoreGive(raceMutex);
clearAllTimes();
updateTimeDisplay();
updateParticipantDisplay();
updateStartLabel();
Serial.println("Force reset dijalankan dari antarmuka web");
request->send(200, "text/plain", "OK");
} else {
request->send(500, "text/plain", "Gagal mendapatkan lock");
}
});
server.begin();
Serial.println("Web server dimulai");
}
// ================================
// CALLBACK LVGL
// ================================
void my_disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
if (spiMutex != NULL && xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
uint32_t w = area->x2 - area->x1 + 1;
uint32_t h = area->y2 - area->y1 + 1;
uint16_t *color_p = (uint16_t *)px_map;
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushColors(color_p, w * h);
tft.endWrite();
xSemaphoreGive(spiMutex);
}
lv_display_flush_ready(disp);
}
void my_touchpad_read(lv_indev_t *indev_driver, lv_indev_data_t *data) {
static unsigned long lastTouchRead = 0;
static bool wasTouched = false;
static unsigned long lastButtonPress = 0;
unsigned long now = millis();
if (now - lastTouchRead < 10) {
data->state = touchPressed ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
if (touchPressed) {
data->point.x = lastX;
data->point.y = lastY;
}
return;
}
lastTouchRead = now;
bool irqActive = (digitalRead(TOUCH_IRQ) == LOW);
if (irqActive) {
if (spiMutex != NULL && xSemaphoreTake(spiMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
if (touch.touched()) {
TS_Point p = touch.getPoint();
if (p.z > 200) {
lastX = map(p.x, TS_MIN_X, TS_MAX_X, 0, SCREEN_WIDTH);
lastY = map(p.y, TS_MIN_Y, TS_MAX_Y, 0, SCREEN_HEIGHT);
lastX = constrain(lastX, 0, SCREEN_WIDTH - 1);
lastY = constrain(lastY, 0, SCREEN_HEIGHT - 1);
data->point.x = lastX;
data->point.y = lastY;
data->state = LV_INDEV_STATE_PR;
if (!wasTouched && (now - lastButtonPress > 100)) {
if (objects.start_time) {
lv_area_t area;
lv_obj_get_coords(objects.start_time, &area);
if (lastX >= area.x1 && lastX <= area.x2 &&
lastY >= area.y1 && lastY <= area.y2) {
Serial.println("[TOUCH] START button pressed");
handleStartButton();
wasTouched = true;
lastButtonPress = now;
}
}
if (objects.reset_time) {
lv_area_t area;
lv_obj_get_coords(objects.reset_time, &area);
if (lastX >= area.x1 && lastX <= area.x2 &&
lastY >= area.y1 && lastY <= area.y2) {
Serial.println("[TOUCH] RESET button pressed");
handleResetButton();
wasTouched = true;
lastButtonPress = now;
}
}
}
touchPressed = true;
xSemaphoreGive(spiMutex);
return;
}
}
xSemaphoreGive(spiMutex);
}
} else {
wasTouched = false;
}
data->state = LV_INDEV_STATE_REL;
touchPressed = false;
}
// ================================
// TASK FREERTOS
// ================================
void uiTask(void *parameter) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xFrequency = pdMS_TO_TICKS(5);
Serial.println("[TASK] uiTask started");
while (true) {
if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(20)) == pdTRUE) {
if (needUpdateStartLabel) {
Serial.println("[UI_TASK] needUpdateStartLabel flag detected!");
updateStartLabel();
needUpdateStartLabel = false;
Serial.println("[UI_TASK] Label updated, flag cleared");
}
lv_timer_handler();
xSemaphoreGive(displayMutex);
}
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
void sensorTask(void *parameter) {
Serial.println("[TASK] sensorTask started");
while (true) {
checkStartSensor();
checkFinishSensor();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void timerTask(void *parameter) {
Serial.println("[TASK] timerTask started");
while (true) {
// π₯ FIX: Increase timeout dari 10ms jadi 50ms
if (xSemaphoreTake(raceMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
if (raceState.isRunning) {
raceState.raceDuration = millis() - raceState.raceStartTime;
}
xSemaphoreGive(raceMutex);
} else {
// Log jika gagal acquire (debugging)
static unsigned long lastWarn = 0;
if (millis() - lastWarn > 5000) {
Serial.println("[TIMER_TASK] β οΈ Failed to acquire mutex");
lastWarn = millis();
}
}
updateTimeDisplay();
updateParticipantDisplay();
// π₯ FIX: Increase delay dari 30ms jadi 50ms (kurangi mutex contention)
vTaskDelay(pdMS_TO_TICKS(50));
}
}
// ================================
// SETUP
// ================================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\nββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β Sistem Timer Balap ESP32 v2.3 β");
Serial.println("β FIXED: HIGHβLOW detection for NC β");
Serial.println("β Mode: Test dengan 1 sensor START β");
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ\n");
pinMode(IR_START_PIN, INPUT_PULLUP);
pinMode(IR_FINISH_PIN, INPUT_PULLUP);
Serial.println("β
Sensor IR (NC) diinisialisasi");
Serial.println("π NC Sensor Logic:");
Serial.println(" HIGH = terhalang/blocked");
Serial.println(" LOW = clear/tidak terhalang");
Serial.println(" Trigger: HIGH β LOW (melewati sensor)\n");
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, LOW);
pinMode(TOUCH_IRQ, INPUT_PULLUP);
tft.begin();
tft.setRotation(1);
tft.fillScreen(TFT_BLACK);
Serial.println("β
TFT diinisialisasi");
touchSPI.begin(TOUCH_CLK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS);
touch.begin(touchSPI);
touch.setRotation(1);
Serial.println("β
Touch diinisialisasi");
if (!LittleFS.begin(true)) {
Serial.println("β οΈ LittleFS Mount Gagal");
} else {
Serial.println("β
LittleFS Terpasang");
loadParticipantConfig();
loadRaceHistory();
}
displayMutex = xSemaphoreCreateMutex();
raceMutex = xSemaphoreCreateMutex();
spiMutex = xSemaphoreCreateMutex();
Serial.println("β
Mutex dibuat");
lv_init();
lv_tick_set_cb([]() { return (uint32_t)millis(); });
display = lv_display_create(SCREEN_WIDTH, SCREEN_HEIGHT);
lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
lv_display_set_buffers(display, buf, NULL, sizeof(buf), LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_flush_cb(display, my_disp_flush);
indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, my_touchpad_read);
Serial.println("β
LVGL diinisialisasi");
ui_init();
Serial.println("β
EEZ UI diinisialisasi");
updateParticipantDisplay();
updateTimeDisplay();
updateStartLabel();
ledcAttach(TFT_BL, TFT_BL_FREQ, TFT_BL_RESOLUTION);
ledcWrite(TFT_BL, TFT_BL_BRIGHTNESS);
Serial.println("β
Backlight NYALA");
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID.c_str(), AP_PASSWORD.c_str());
delay(100);
Serial.println("\nββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β WiFi AP Dimulai β");
Serial.println("β βββββββββββββββββββββββββββββββββββββββββ£");
Serial.println("β SSID: " + AP_SSID);
Serial.println("β Pass: " + AP_PASSWORD);
Serial.println("β IP: " + WiFi.softAPIP().toString());
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ\n");
setupWebServer();
xTaskCreatePinnedToCore(uiTask, "UI", 8192, NULL, 1, &uiTaskHandle, 1); // Priority 1, Core 1
xTaskCreatePinnedToCore(sensorTask, "Sensor", 8192, NULL, 4, &sensorTaskHandle, 0); // Priority 4 (HIGHEST), Core 0
xTaskCreatePinnedToCore(timerTask, "Timer", 4096, NULL, 2, &timerTaskHandle, 0); // Priority 2, Core 0
Serial.println("\nββββββββββββββββββββββββββββββββββββββββββ");
Serial.println("β π― SISTEM SIAP! π― β");
Serial.println("β βββββββββββββββββββββββββββββββββββββββββ£");
Serial.println("β 1. Tekan START di LCD β");
Serial.println("β 2. Lewati sensor START (HIGHβLOW) β");
Serial.println("β 3. Timer akan jalan otomatis! β");
Serial.println("β 4. Test dengan 1 sensor dulu β");
Serial.println("ββββββββββββββββββββββββββββββββββββββββββ\n");
Serial.println("π Debug mode aktif - monitoring sensor state...\n");
}
void loop() {
vTaskDelay(pdMS_TO_TICKS(1000));
}