Ir sensor never triggers time

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));
}

Does the IR sensor work in a minimal sketch?

Hi, @bimosora
When did you find this out, at what stage of writing your code?

You did write your code in stages I hope.
You should have a bit of code that JUST tests the connection and signal from the sensor.

Can you post a link to specs and data of the IR sensor?

How have you got the sensor connected and powered in your circuit?

Can you please post a copy of your circuit, a picture of a hand drawn circuit in jpg, png?
Hand drawn and photographed is perfectly acceptable.
Please include ALL hardware, power supplies, component names and pin labels.

Thanks.. Tom.... :smiley: :+1: :coffee: :australia:

That's a lot of code, and some of it is even a bit advanced. It is strange that you can't get the IR sensor to work.
Where is the IR sensor library? Did you forget it? Once that is in place, start with a minimal sketch to learn how to turn it on and off, then work that into your big sketch.

Alternatively, does replacing the IR sensor with a pushbutton work?

That is to ask if it is a sensor issue at all.

Since IR sensors work, mostly, you should check if yours does, or ever did.

Since you certainly did that before rattling the cage here, it's in the logic. :expressionless:

Use your usual technique for finding logic errors. As a simple start, read the code, make it chatty to confirm the values of key variables and that they are properly informing flow through your code.

a7

1 Like

Hi, I'm having a problem with the checkstartsensor() and checkstartfinish() sections. Once the sensor has changed to high and low, there's no call there, even though it should be. That's why time_run is not called

Pin sensor 21, 22. Connected to NC IR Break Beam

I replaced it with a normal push button, so the problem is that the sensor does not trigger the time_run action.

So display the value you are getting from your push button code.

Sry, I can't tell what you mean.

#define IR_START_PIN 21
#define IR_FINISH_PIN 22

Please describe this IR sensor that uses two input pins.

Or maybe you have two IR sensors and you are trying to get one of them to work?

This

    if (lastStartState == HIGH && currentState == LOW) {

is not what the comment says. This

    if (lastStartState != currentState) {

actually detects changes. Then you must go on to see what (currentState* is to see which edge just transitioned.

It's a bad switch handler.

But… do you need to debounce an IR sensor? The units I use turn on and off cleanly.

a7

Some IR "line followers" can output a HIGH on "sensed" some can output a "LOW" on "sensed." Check your module.

I posted in haste.

In your check function one state variable is being asked to represent two different ideas at the same time.

A debounce pattern must keep two separate β€œstate” variables, the previous raw reading, and the debounced (stable) state.

Your function is a bungled attempt at the debouncing code featured here:

https://docs.arduino.cc/built-in-examples/digital/Debounce/

In your case, it could look like this

void checkStartSensor() {
  static bool startSwitchState = HIGH;   // debounced state (make global if used elsewhere)

  bool currentState = digitalRead(IR_START_PIN);   // raw reading (may bounce)

 // If raw changed, reset debounce timer.
  if (currentState != lastStartState) {
    lastStartDebounce = millis();
  }

 // Save raw reading for next pass (Limor's lastButtonState = reading)
  lastStartState = currentState;

 // If raw has been stable long enough, accept it as the debounced state.
  if ((millis() - lastStartDebounce) > DEBOUNCE_DELAY) {

   // Debounced transition occurred (Limor's "reading != buttonState")
    if (currentState != startSwitchState) {
      startSwitchState = currentState;

     // Select ONE edge: falling (assuming LOW = active)
      if (startSwitchState == LOW) {

// dozens of lines of code to execute on the detected edge

      }
    }
  }
}

Instead of piling dozens of lines of code into the middle of your IR handling, I strongly suggest to you to create a bool flag and set it on the edge

     if (startSwitchState == LOW) {
        startEdgeFlag = true;              // set flag, do the heavy work elsewhere
     }

and elsewhere act on the flag, being sure to consume it when you do, so your code might read

  checkStartSensor();

  if (startEdgeFlag) {
    startEdgeFlag = false;   // consume the event

 // dozens of lines of code to execute

  }

Or skip the flag by having checkStartSensor() return a bool

  if (checkStartSensor()) {
    startEdgeFlag = false;   // consume the event

 // dozens of lines of code to execute

  }

I haven't looked at your > 1000 lines of code, but dividing things makes developing easier. You could test the IR here, for example, in the total absence of all that unrelated code and in fact with not much other code at all and only the IR sensor for extra hardware.

a7

I'm wondering how much of this code @bimosora actually wrote. Multi-tasking code running on FreeRTOS with a web server isn't typically written by someone who can't trigger something to happen on an external input.

2 Likes