Why does the ILI9488 XPT2046 4inch TFT display behave like a mirror when touched?

I want to create a line_select state consisting of 10 boxes, namely 1A, 1B, 2A, 2B, 3A, 3B, 4A, 4B, 5A, and 5B, the coding was successfully executed and displayed the display as I wanted, but why does it mirror when clicked?

So when I click 1A, it'll be pressed on 2B. If I press 1B, it'll be pressed on 2A. And so on.

I've tried various ways to fix it, but it still doesn't work. Can anyone help?

// =====================================================================
// === KODE FINAL V10.19 - SOLUSI GEOMETRI MATEMATIS & KOREKSI URUTAN DISPLAY ===
// =====================================================================

#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>

// --- Definisi Warna ---
#define BLACK       0x0000
#define BLUE        0x001F
#define GREEN       0x07E0
#define WHITE       0xFFFF
#define RED         0xF800
#define YELLOW      0xFFE0

// --- 1. DEFINISI PIN TOUCH ---
#define TOUCH_CS 4 
#define TOUCH_IRQ 5   

TFT_eSPI tft = TFT_eSPI();
XPT2046_Touchscreen ts(TOUCH_CS); 

// --- 2. DEFINISI STATE MACHINE & DATA ---
enum State {
    STATE_LINE_SELECT,
    STATE_PART_SELECT 
};

State currentState = STATE_LINE_SELECT;

// Array Line Names (Index 0-3 adalah Baris 1/Atas, Index 8-9 adalah Baris 3/Bawah)
const char* lineNames[] = {
    "1A", "1B", "2A", "2B", // Index 0-3 (Baris 1/ATAS)
    "3A", "3B", "4A", "4B", // Index 4-7 (Baris 2/TENGAH)
    "5A", "5B"              // Index 8-9 (Baris 3/BAWAH)
};
const int TOTAL_LINES = 10;

// =======================================================
// === NILAI KALIBRASI RAW (Dipertahankan dari V10.18) ===
// =======================================================
// Nilai ini harus diverifikasi. Jika sentuhan melenceng, tukar/balik nilainya.
#define TS_LEFT 3946    // RAW X MIN 
#define TS_RT 190       // RAW X MAX 
#define TS_TOP 219      // RAW Y MIN 
#define TS_BOT 3935     // RAW Y MAX 

#define X_CAL 480 // LEBAR PIKSEL LAYAR (Rotasi 1)
#define Y_CAL 320 // TINGGI PIKSEL LAYAR (Rotasi 1)

// Variabel Kontrol
bool touch_initialized = false;
unsigned long last_touch_time = 0; 
const unsigned long DEBOUNCE_DELAY = 300; 
#define TOUCH_PRESSURE_THRESHOLD 500 

// --- Konstanta Geometri Tombol ---
const int BTN_W = 113; 
const int BTN_H = 80;  
const int GAP_H = 5;   
const int GAP_V = 15;  
const int START_X = 6; 
const int START_Y = 45; 

// ===============================================
// === FUNGSI UTAMA DISPLAY & STATE MACHINE ===
// ===============================================

void drawButton(int x, int y, int w, int h, const char* label, int textSize, uint16_t bgColor, uint16_t fgColor) {
    tft.fillRect(x, y, w, h, bgColor);
    tft.drawRect(x, y, w, h, fgColor);
    tft.setTextColor(fgColor, bgColor);
    tft.setTextSize(textSize);
    uint16_t text_w = tft.textWidth(label); 
    uint16_t text_h = 8 * textSize;
    int label_x = x + (w - text_w) / 2;
    int label_y = y + (h - text_h) / 2;
    tft.setCursor(label_x, label_y);
    tft.print(label);
}

/**
 * @brief Menggambar menu pemilihan baris dengan urutan yang BENAR (1A di Atas).
 */
void drawLineSelectMenu() {
    tft.fillScreen(BLACK);
    tft.setTextColor(WHITE, BLACK);
    tft.setTextSize(3);
    uint16_t title_width = tft.textWidth("SELECT LINE");
    tft.setCursor(tft.width() / 2 - title_width / 2, 10); 
    tft.print("PILIH LINE");
    
    // i: Baris Display (0 = Atas, 2 = Bawah)
    for(int i = 0; i < 3; i++) { 
        // j: Kolom Display (0 = Kiri, 3 = Kanan)
        for(int j = 0; j < 4; j++) { 
            
            // KOREKSI URUTAN DISPLAY:
            // i=0 (Baris Atas) -> line_idx 0-3 (1A, 1B, 2A, 2B)
            // i=1 (Baris Tengah) -> line_idx 4-7 (3A, 3B, 4A, 4B)
            // i=2 (Baris Bawah) -> line_idx 8-11 (5A, 5B, [kosong], [kosong])
            int line_idx = i * 4 + j; 
            
            if (line_idx < TOTAL_LINES) {
                int x = START_X + j * (BTN_W + GAP_H);
                int y = START_Y + i * (BTN_H + GAP_V);
                drawButton(x, y, BTN_W, BTN_H, lineNames[line_idx], 2, BLUE, WHITE);
            }
        }
    }
}

void drawPartSelectMenu(const char* selectedLine) {
    tft.fillScreen(BLACK);
    tft.setTextColor(GREEN, BLACK);
    tft.setTextSize(3);
    
    uint16_t title_width = tft.textWidth("MENU PART SELECT");
    tft.setCursor(tft.width() / 2 - title_width / 2, 100); 
    tft.print("MENU PART SELECT");
    
    tft.setTextColor(WHITE, BLACK);
    tft.setTextSize(5);
    tft.setCursor(tft.width() / 2 - tft.textWidth(selectedLine) / 2, 150);
    tft.print(selectedLine);
    
    // Tombol Back
    drawButton(190, 250, 100, 50, "KEMBALI", 2, RED, WHITE);
}

void changeState(State newState, const char* selectedLine = "") {
    currentState = newState;
    tft.fillScreen(BLACK); 
    switch(currentState) {
        case STATE_LINE_SELECT:
            drawLineSelectMenu();
            Serial.println("State: LINE_SELECT");
            break;
        case STATE_PART_SELECT:
            drawPartSelectMenu(selectedLine);
            Serial.print("State: PART_SELECT, Line: ");
            Serial.println(selectedLine);
            break;
    }
}

// ===============================================
// === FUNGSI HANDLE TOUCH - KOREKSI AXIS & GEOMETRI V10.19 ===
// ===============================================

void handleTouch() {
    if (millis() - last_touch_time < DEBOUNCE_DELAY) {
        return; 
    }

    if (ts.touched()) {
        last_touch_time = millis();
        TS_Point p = ts.getPoint();

        if (p.z < TOUCH_PRESSURE_THRESHOLD) {
            return; 
        }

        // --- PEMETAAN PIKSEL (HIPOTESIS KOREKSI AXIS V10.19) ---
        
        int px = map(p.x, TS_LEFT, TS_RT, X_CAL, 0); // X Display (0-480) <- Raw X, Inverted
        int py = map(p.y, TS_TOP, TS_BOT, Y_CAL, 0); // Y Display (0-320) <- Raw Y, Inverted
        
        // Batasan: memastikan koordinat berada di dalam rentang layar
        px = max(0, min(px, X_CAL - 1));
        py = max(0, min(py, Y_CAL - 1));

        // --- DEBUG VISUAL (Titik Kuning) ---
        tft.fillCircle(px, py, 5, YELLOW); 
        Serial.print("Touch FINAL Pixels: (");
        Serial.print(px);
        Serial.print(", ");
        Serial.print(py);
        Serial.println(")");
        
        // --- Logika Deteksi Tombol Berbasis Perhitungan Geometri Langsung ---
        
        if (currentState == STATE_LINE_SELECT) {
            
            int i = -1; // Baris Display (0=Atas, 2=Bawah)
            int j = -1; // Kolom Display (0=Kiri, 3=Kanan)

            // 1. Hitung Kolom j (0 hingga 3)
            if (px >= START_X && px < (START_X + 4*BTN_W + 3*GAP_H)) {
                 int block_x = px - START_X; 
                 j = block_x / (BTN_W + GAP_H); 
                 
                 // Pengecekan Gap
                 int button_start_x = START_X + j * (BTN_W + GAP_H);
                 if (px > (button_start_x + BTN_W) && j < 4) {
                    j = -1; // Berada di GAP
                 }
            }

            // 2. Hitung Baris i (0 hingga 2)
            if (py >= START_Y && py < (START_Y + 3*BTN_H + 2*GAP_V)) {
                int block_y = py - START_Y;
                i = block_y / (BTN_H + GAP_V);
                
                // Pengecekan Gap
                int button_start_y = START_Y + i * (BTN_H + GAP_V);
                if (py > (button_start_y + BTN_H) && i < 3) {
                    i = -1; // Berada di GAP
                }
            }
            
            // 3. Jika i dan j valid, hitung line_idx
            if (i != -1 && j != -1) {
                
                // KOREKSI INDEX V10.19: Baris i (0=Atas) langsung memetakan ke line_idx 0-3.
                int line_idx = i * 4 + j; 
                
                if (line_idx < TOTAL_LINES) {
                    Serial.print("SUCCESS! Baris:");
                    Serial.print(i);
                    Serial.print(", Kolom:");
                    Serial.print(j);
                    Serial.print(", Line Index:");
                    Serial.print(line_idx);
                    Serial.print(" -> ");
                    Serial.println(lineNames[line_idx]);

                    // Menonaktifkan Titik Kuning di Area Tombol yang Berhasil Ditekan
                    tft.fillCircle(px, py, 5, BLACK);

                    changeState(STATE_PART_SELECT, lineNames[line_idx]);
                    return; 
                }
            } else {
                 // Menonaktifkan Titik Kuning di Area yang Tidak Ada Tombol (untuk feedback visual)
                tft.fillCircle(px, py, 5, BLACK);
            }
        }
        
        // --- Logika Tombol Kembali (BACK) ---
        else if (currentState == STATE_PART_SELECT) {
             // Area Tombol KEMBALI: x=190, y=250, w=100, h=50
            if (px >= 190 && px <= 290 && py >= 250 && py <= 300) {
                Serial.println("Tombol KEMBALI ditekan");
                changeState(STATE_LINE_SELECT);
                return;
            }
            tft.fillCircle(px, py, 5, BLACK); // Hapus titik kuning setelah cek
        }
    }
}

// ===============================================
// === SETUP DAN LOOP ===
// ===============================================

void setup() {
    Serial.begin(115200);
    Serial.println("TFT/Touch V10.19 Started - FINAL GEOMETRY & ORDER FIX");

    tft.init();
    tft.setRotation(1); // Mode Landscape (480x320)
    tft.fillScreen(BLACK);

    changeState(STATE_LINE_SELECT);
}

void loop() {
    if (!touch_initialized) {
        ts.begin();
        touch_initialized = true; 
    }
    
    handleTouch();
}

Try swapping TS_LEFT and TS_RT in the call to map(), that should reverse the X axis of the touchscreen. Alternatively, X_CAL and 0 could be swapped.

Even with no rotation, the touchscreen origin doesn't necessarily match the display origin. So a maximum display X value may correspond to a minimum touchscreen X value; same thing for Y values. And when you rotate the display, as you've done, you'll have to rotate the touchscreen in code as well, and with 90/270 degree rotations, display X/Y maps to touchscreen Y/X.

Your code actually makes note of that (translated):

// These values ​​must be verified. If the touch is off, swap/reverse the values.

@david_2018 @van_der_decken
Thank you very much for your help. It's enlightening for my project.

To solve this problem, I re-calibrated the touch_calibrate and obtained the following values.

// Use this calibration code in setup():
uint16_t calData[5] = { 226, 3732, 233, 3636, 7 };
tft.setTouch(calData);

I then applied these results to the previous code and obtained the corresponding press point for the box button. This is the result.

// =====================================================================
// === KODE FINAL V10.21 - KALIBRASI TERBARU (X: 226, 3732; Y: 233, 3636) ===
// =====================================================================

#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>

// --- Definisi Warna ---
#define BLACK      0x0000
#define BLUE       0x001F
#define GREEN      0x07E0
#define WHITE      0xFFFF
#define RED        0xF800
#define YELLOW     0xFFE0

// --- 1. DEFINISI PIN TOUCH ---
// PASTIKAN PIN CS DAN IRQ INI SESUAI DENGAN WIRING DI BOARD ANDA
#define TOUCH_CS 4  
#define TOUCH_IRQ 5   

TFT_eSPI tft = TFT_eSPI();
XPT2046_Touchscreen ts(TOUCH_CS); 

// --- 2. DEFINISI STATE MACHINE & DATA ---
enum State {
    STATE_LINE_SELECT,
    STATE_PART_SELECT 
};

State currentState = STATE_LINE_SELECT;

// Array Line Names 
const char* lineNames[] = {
    "1A", "1B", "2A", "2B", // Index 0-3 (Baris 1/ATAS)
    "3A", "3B", "4A", "4B", // Index 4-7 (Baris 2/TENGAH)
    "5A", "5B"              // Index 8-9 (Baris 3/BAWAH)
};
const int TOTAL_LINES = 10;

// =======================================================
// === NILAI KALIBRASI RAW - DISESUAIKAN DENGAN HASIL TERBARU ANDA ===
// uint16_t calData[5] = { 226, 3732, 233, 3636, 7 };
// =======================================================
#define TS_LEFT 226     // RAW X MIN (Dari calData[0])
#define TS_RT 3732      // RAW X MAX (Dari calData[1])
#define TS_TOP 233      // RAW Y MIN (Dari calData[2])
#define TS_BOT 3636     // RAW Y MAX (Dari calData[3]) 

#define X_CAL 480 // LEBAR PIKSEL LAYAR (Rotasi 1)
#define Y_CAL 320 // TINGGI PIKSEL LAYAR (Rotasi 1)

// Variabel Kontrol
bool touch_initialized = false;
unsigned long last_touch_time = 0;  
const unsigned long DEBOUNCE_DELAY = 300; 
#define TOUCH_PRESSURE_THRESHOLD 500 

// --- Konstanta Geometri Tombol ---
const int BTN_W = 113;  
const int BTN_H = 80;   
const int GAP_H = 5;    // Jarak Horizontal antar tombol
const int GAP_V = 15;   // Jarak Vertikal antar tombol
const int START_X = 6;  // Koordinat X Awal Grid Tombol
const int START_Y = 45; // Koordinat Y Awal Grid Tombol

// ===============================================
// === FUNGSI UTAMA DISPLAY & STATE MACHINE ===
// ===============================================

void drawButton(int x, int y, int w, int h, const char* label, int textSize, uint16_t bgColor, uint16_t fgColor) {
    tft.fillRect(x, y, w, h, bgColor);
    tft.drawRect(x, y, w, h, fgColor);
    tft.setTextColor(fgColor, bgColor);
    tft.setTextSize(textSize);
    uint16_t text_w = tft.textWidth(label); 
    uint16_t text_h = 8 * textSize;
    int label_x = x + (w - text_w) / 2;
    int label_y = y + (h - text_h) / 2;
    tft.setCursor(label_x, label_y);
    tft.print(label);
}

/**
 * @brief Menggambar menu pemilihan baris.
 */
void drawLineSelectMenu() {
    tft.fillScreen(BLACK);
    tft.setTextColor(WHITE, BLACK);
    tft.setTextSize(3);
    uint16_t title_width = tft.textWidth("SELECT LINE");
    tft.setCursor(tft.width() / 2 - title_width / 2, 10); 
    tft.print("SELECT LINE");
    
    // i: Baris Display (0 = Atas, 2 = Bawah)
    for(int i = 0; i < 3; i++) { 
        // j: Kolom Display (0 = Kiri, 3 = Kanan)
        for(int j = 0; j < 4; j++) { 
            
            int line_idx = i * 4 + j; 
            
            if (line_idx < TOTAL_LINES) {
                int x = START_X + j * (BTN_W + GAP_H);
                int y = START_Y + i * (BTN_H + GAP_V);
                drawButton(x, y, BTN_W, BTN_H, lineNames[line_idx], 2, BLUE, WHITE);
            }
        }
    }
}

void drawPartSelectMenu(const char* selectedLine) {
    tft.fillScreen(BLACK);
    tft.setTextColor(GREEN, BLACK);
    tft.setTextSize(3);
    
    uint16_t title_width = tft.textWidth("MENU PART SELECT");
    tft.setCursor(tft.width() / 2 - title_width / 2, 100); 
    tft.print("MENU PART SELECT");
    
    tft.setTextColor(WHITE, BLACK);
    tft.setTextSize(5);
    tft.setCursor(tft.width() / 2 - tft.textWidth(selectedLine) / 2, 150);
    tft.print(selectedLine);
    
    // Tombol Back
    drawButton(190, 250, 100, 50, "KEMBALI", 2, RED, WHITE);
}

void changeState(State newState, const char* selectedLine = "") {
    currentState = newState;
    tft.fillScreen(BLACK); 
    switch(currentState) {
        case STATE_LINE_SELECT:
            drawLineSelectMenu();
            Serial.println("State: LINE_SELECT");
            break;
        case STATE_PART_SELECT:
            drawPartSelectMenu(selectedLine);
            Serial.print("State: PART_SELECT, Line: ");
            Serial.println(selectedLine);
            break;
    }
}

// ===============================================
// === FUNGSI HANDLE TOUCH - MENGGUNAKAN DATA KALIBRASI RAW ===
// ===============================================

void handleTouch() {
    // Debounce
    if (millis() - last_touch_time < DEBOUNCE_DELAY) {
        return; 
    }

    if (ts.touched()) {
        last_touch_time = millis();
        TS_Point p = ts.getPoint();

        if (p.z < TOUCH_PRESSURE_THRESHOLD) {
            return; 
        }

        // --- PEMETAAN PIKSEL MENGGUNAKAN NILAI KALIBRASI BARU ---
        
        // Memetakan Raw X (TS_LEFT hingga TS_RT) ke Display X (X_CAL hingga 0)
        int px = map(p.x, TS_LEFT, TS_RT, X_CAL, 0); 
        
        // Memetakan Raw Y (TS_TOP hingga TS_BOT) ke Display Y (Y_CAL hingga 0)
        int py = map(p.y, TS_TOP, TS_BOT, Y_CAL, 0); 
        
        // Batasan: memastikan koordinat berada di dalam rentang layar
        px = max(0, min(px, X_CAL - 1));
        py = max(0, min(py, Y_CAL - 1));

        // --- DEBUG VISUAL (Titik Kuning) ---
        tft.fillCircle(px, py, 5, YELLOW); 
        Serial.print("Touch FINAL Pixels: (");
        Serial.print(px);
        Serial.print(", ");
        Serial.print(py);
        Serial.println(")");
        
        // --- Logika Deteksi Tombol Line Select ---
        if (currentState == STATE_LINE_SELECT) {
            
            int i = -1; // Baris Display (0=Atas, 2=Bawah)
            int j = -1; // Kolom Display (0=Kiri, 3=Kanan)

            // 1. Hitung Kolom j (0 hingga 3)
            if (px >= START_X && px < (START_X + 4*BTN_W + 3*GAP_H)) {
                 int block_x = px - START_X; 
                 j = block_x / (BTN_W + GAP_H); 
                 
                 // Pengecekan Gap Horizontal
                 int button_start_x = START_X + j * (BTN_W + GAP_H);
                 if (px > (button_start_x + BTN_W) && j < 4) {
                     j = -1; // Berada di GAP
                 }
            }

            // 2. Hitung Baris i (0 hingga 2)
            if (py >= START_Y && py < (START_Y + 3*BTN_H + 2*GAP_V)) {
                int block_y = py - START_Y;
                i = block_y / (BTN_H + GAP_V);
                
                // Pengecekan Gap Vertikal
                int button_start_y = START_Y + i * (BTN_H + GAP_V);
                if (py > (button_start_y + BTN_H) && i < 3) {
                    i = -1; // Berada di GAP
                }
            }
            
            // 3. Jika i dan j valid, hitung line_idx
            if (i != -1 && j != -1) {
                
                int line_idx = i * 4 + j; 
                
                if (line_idx < TOTAL_LINES) {
                    Serial.print("SUCCESS! Line Index:");
                    Serial.print(line_idx);
                    Serial.print(" -> ");
                    Serial.println(lineNames[line_idx]);

                    // Menonaktifkan Titik Kuning di Area Tombol yang Berhasil Ditekan
                    tft.fillCircle(px, py, 5, BLACK);

                    changeState(STATE_PART_SELECT, lineNames[line_idx]);
                    return; 
                }
            } else {
                 // Menonaktifkan Titik Kuning di Area yang Tidak Ada Tombol
                tft.fillCircle(px, py, 5, BLACK);
            }
        }
        
        // --- Logika Tombol Kembali (BACK) ---
        else if (currentState == STATE_PART_SELECT) {
             // Area Tombol KEMBALI: x=190, y=250, w=100, h=50
            if (px >= 190 && px <= 290 && py >= 250 && py <= 300) {
                Serial.println("Tombol KEMBALI ditekan");
                changeState(STATE_LINE_SELECT);
                return;
            }
            tft.fillCircle(px, py, 5, BLACK); // Hapus titik kuning setelah cek
        }
    }
}

// ===============================================
// === SETUP DAN LOOP ===
// ===============================================

void setup() {
    Serial.begin(115200);
    Serial.println("TFT/Touch V10.21 Started - Kalibrasi Terbaru Diterapkan");

    tft.init();
    tft.setRotation(1); // Mode Landscape (480x320)
    tft.fillScreen(BLACK);

    changeState(STATE_LINE_SELECT);
}

void loop() {
    if (!touch_initialized) {
        ts.begin();
        touch_initialized = true; 
    }
    
    handleTouch();
}