// ESP32-C6 + ILI9341 + FT6206 + IR receiver + optional IR send
// Matches your Wokwi wiring JSON: IR DAT -> ESP pin 9
// I2C SDA = 2, SCL = 3
// TFT: CS=10, DC=11, RST=12
// SPI pins: SCK=5, MOSI=6, MISO=7
// Libraries required:
// Adafruit_ILI9341, Adafruit_GFX, Adafruit_FT6206, Preferences, IRremote
#include <SPI.h>
#include <Wire.h>
#include <Preferences.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <Adafruit_FT6206.h>
#include <IRremote.h>
#define TFT_CS 10
#define TFT_DC 11
#define TFT_RST 12
#define SPI_SCK 5
#define SPI_MOSI 6
#define SPI_MISO 7
#define I2C_SDA 2
#define I2C_SCL 3
// According to your Wokwi JSON mapping, IR DAT is wired to ESP pin 9
#define IR_RX_PIN 9
#define IR_TX_PIN 13 // optional transmitter pin (use transistor/driver for real IR LED)
// TFT + touch objects
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
Adafruit_FT6206 ctp = Adafruit_FT6206();
Preferences prefs;
// affine coefficients
double a_coef=1, b_coef=0, c_coef=0, d_coef=0, e_coef=1, f_coef=0;
bool affine_valid = false;
struct RawPt { int x; int y; };
unsigned long lastTouchTime = 0;
const unsigned long clearTimeout = 3500;
// IR receiver/sender
IRrecv irrecv(IR_RX_PIN);
IRsend irsend(IR_TX_PIN);
decode_results irResults;
bool irPrintEnabled = false;
unsigned long lastIRTime = 0;
uint32_t lastIRValue = 0;
String lastIRProto = "NONE";
// Remote map entry
struct IRMap { const char* name; uint32_t code; };
IRMap remoteMap[] = {
{"Power", 0xFFA25D},
{"Menu", 0xFFE21D},
{"Test", 0xFF22DD},
{"Plus", 0xFF02FD},
{"Back", 0xFFC23D},
{"Previous",0xFFE01F},
{"Play", 0xFFA857},
{"Next", 0xFF906F},
{"0", 0xFF6897},
{"Minus", 0xFF9867},
{"C", 0xFFB04F},
{"1", 0xFF30CF},
{"2", 0xFF18E7},
{"3", 0xFF7A85},
{"4", 0xFF10EF},
{"5", 0xFF38C7},
{"6", 0xFF5AA5},
{"7", 0xFF42BD},
{"8", 0xFF4AB5},
{"9", 0xFF52AD}
};
const int REMOTE_MAP_COUNT = sizeof(remoteMap)/sizeof(remoteMap[0]);
// ---------- Utils ----------
void showMessage(const char* s) {
tft.fillRect(0,0, tft.width(), 28, ILI9341_BLACK);
tft.setCursor(6,4);
tft.setTextSize(2);
tft.setTextColor(ILI9341_CYAN);
tft.print(s);
}
void drawCross(int x,int y,uint16_t color=ILI9341_YELLOW) {
tft.drawLine(x-12,y,x+12,y,color);
tft.drawLine(x,y-12,x,y+12,color);
tft.drawCircle(x,y,6,color);
}
// Average N samples for stable raw reading
bool getAveragedRaw(RawPt &out, int samples=6, int timeout_ms=5000) {
unsigned long start = millis();
int got = 0;
long sx=0, sy=0;
while (millis() - start < (unsigned long)timeout_ms && got < samples) {
if (ctp.touched()) {
TS_Point p = ctp.getPoint();
sx += p.x; sy += p.y;
got++;
delay(30);
while (ctp.touched()) { delay(8); }
}
delay(8);
}
if (got == 0) return false;
out.x = (int)(sx / got);
out.y = (int)(sy / got);
return true;
}
// ---------- Affine solve (least squares) ----------
bool solve3x3(double S[3][3], double B[3], double out[3]) {
double A[3][4];
for (int i=0;i<3;i++){
for (int j=0;j<3;j++) A[i][j]=S[i][j];
A[i][3]=B[i];
}
for (int i=0;i<3;i++){
int piv=i;
for (int r=i+1;r<3;r++) if (fabs(A[r][i])>fabs(A[piv][i])) piv=r;
if (fabs(A[piv][i]) < 1e-12) return false;
if (piv!=i) for (int c=0;c<4;c++) { double tmp=A[i][c]; A[i][c]=A[piv][c]; A[piv][c]=tmp; }
double pv = A[i][i];
for (int c=i;c<4;c++) A[i][c] /= pv;
for (int r=i+1;r<3;r++){
double f = A[r][i];
for (int c=i;c<4;c++) A[r][c] -= f * A[i][c];
}
}
for (int i=2;i>=0;i--){
double val = A[i][3];
for (int c=i+1;c<3;c++) val -= A[i][c] * out[c];
out[i] = val;
}
return true;
}
bool computeAffine(const RawPt raw[], const int screenX[], const int screenY[], int N) {
if (N < 3) return false;
double S[3][3] = {0}, Tx[3] = {0}, Ty[3] = {0};
for (int i=0;i<N;i++){
double rx = raw[i].x;
double ry = raw[i].y;
double row[3] = {rx, ry, 1.0};
for (int r=0;r<3;r++) for (int c=0;c<3;c++) S[r][c] += row[r]*row[c];
Tx[0] += row[0]*screenX[i];
Tx[1] += row[1]*screenX[i];
Tx[2] += row[2]*screenX[i];
Ty[0] += row[0]*screenY[i];
Ty[1] += row[1]*screenY[i];
Ty[2] += row[2]*screenY[i];
}
double px[3], py[3];
if (!solve3x3(S, Tx, px)) return false;
if (!solve3x3(S, Ty, py)) return false;
a_coef = px[0]; b_coef = px[1]; c_coef = px[2];
d_coef = py[0]; e_coef = py[1]; f_coef = py[2];
affine_valid = true;
prefs.begin("affine", false);
prefs.putDouble("a", a_coef); prefs.putDouble("b", b_coef); prefs.putDouble("c", c_coef);
prefs.putDouble("d", d_coef); prefs.putDouble("e", e_coef); prefs.putDouble("f", f_coef);
prefs.putBool("valid", true);
prefs.end();
return true;
}
void loadAffine() {
prefs.begin("affine", true);
affine_valid = prefs.getBool("valid", false);
if (affine_valid) {
a_coef = prefs.getDouble("a", 1.0);
b_coef = prefs.getDouble("b", 0.0);
c_coef = prefs.getDouble("c", 0.0);
d_coef = prefs.getDouble("d", 0.0);
e_coef = prefs.getDouble("e", 1.0);
f_coef = prefs.getDouble("f", 0.0);
Serial.println("Loaded affine coefficients:");
Serial.printf("a=%.6f b=%.6f c=%.6f\n", a_coef,b_coef,c_coef);
Serial.printf("d=%.6f e=%.6f f=%.6f\n", d_coef,e_coef,f_coef);
} else {
Serial.println("No affine coeffs saved.");
}
prefs.end();
}
void clearAffine() {
prefs.begin("affine", false);
prefs.clear();
prefs.end();
affine_valid = false;
Serial.println("Affine calibration cleared.");
}
// ---------- 9-point calibration ----------
bool run9PointCalibration() {
tft.fillScreen(ILI9341_BLACK);
showMessage("9-point Cal: Touch points");
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(6,32);
tft.println("Touch crosshair accurately");
int margin = 18;
int xs[3] = { margin, tft.width()/2, tft.width()-margin };
int ys[3] = { margin, tft.height()/2, tft.height()-margin };
const int N = 9;
RawPt raw[N];
int targetX[N], targetY[N];
int idx = 0;
for (int ry=0; ry<3; ry++) {
for (int rx=0; rx<3; rx++) {
int tx = xs[rx], ty = ys[ry];
tft.fillRect(0,48, tft.width(), tft.height()-48, ILI9341_BLACK);
showMessage("Touch point");
drawCross(tx, ty, ILI9341_YELLOW);
Serial.print("Please touch point "); Serial.print(idx+1);
Serial.print(" at screen "); Serial.print(tx); Serial.print(","); Serial.println(ty);
RawPt avg;
bool ok = getAveragedRaw(avg, 6, 10000); // 6 samples, 10s timeout
if (!ok) {
Serial.println("Timeout collecting sample. Calibration aborted.");
tft.fillScreen(ILI9341_BLACK);
showMessage("Calibration aborted (timeout)");
return false;
}
raw[idx] = avg;
targetX[idx] = tx;
targetY[idx] = ty;
Serial.print("Sample "); Serial.print(idx+1);
Serial.print(" raw="); Serial.print(avg.x); Serial.print(","); Serial.println(avg.y);
idx++;
delay(250);
}
}
bool ok = computeAffine(raw, targetX, targetY, N);
if (!ok) {
Serial.println("Affine solve failed.");
showMessage("Cal failed (solve)");
return false;
}
Serial.println("Affine calibration successful and saved.");
tft.fillScreen(ILI9341_BLACK);
showMessage("Calibration saved");
delay(600);
return true;
}
// Map raw -> screen
void rawToScreen(const RawPt &raw, int &sx, int &sy) {
if (affine_valid) {
double rx = raw.x, ry = raw.y;
double tx = a_coef*rx + b_coef*ry + c_coef;
double ty = d_coef*rx + e_coef*ry + f_coef;
sx = (int) round(tx);
sy = (int) round(ty);
sx = constrain(sx, 0, tft.width()-1);
sy = constrain(sy, 0, tft.height()-1);
} else {
sx = raw.x; sy = raw.y;
}
}
// I2C scanner
void i2cScannerOnce() {
Serial.println(F("=== I2C Scanner ==="));
Wire.begin(I2C_SDA, I2C_SCL);
delay(50);
bool any=false;
for (uint8_t addr=1; addr<127; addr++){
Wire.beginTransmission(addr);
uint8_t err = Wire.endTransmission();
if (err==0) { Serial.print("I2C device at 0x"); Serial.println(addr, HEX); any=true;}
}
if (!any) Serial.println("No I2C devices found");
Serial.println(F("==================="));
}
// --- IR helpers ---
const char* lookupNameForCode(uint32_t code) {
for (int i=0;i<REMOTE_MAP_COUNT;i++){
if (remoteMap[i].code == code) return remoteMap[i].name;
}
return NULL;
}
void displayIROnTFT(uint32_t code, const char* proto, const char* name) {
char buf[64];
tft.fillRect(0,0, tft.width(), 48, ILI9341_BLACK);
tft.setCursor(6,4);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.print("IR: ");
if (name) tft.print(name);
else tft.print("0x"), tft.print(code, HEX);
tft.setCursor(6,26);
tft.setTextSize(1);
snprintf(buf, sizeof(buf), "%s 0x%08X", proto?proto:"UNK", (unsigned)code);
tft.setTextColor(ILI9341_CYAN);
tft.print(buf);
}
// ---------- UI intro ----------
void tftIntro() {
tft.setRotation(0); // portrait
tft.fillScreen(ILI9341_BLACK);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(10,10);
tft.println("ILI9341 + FT6206 + IR");
delay(120);
tft.fillScreen(ILI9341_RED); delay(80);
tft.fillScreen(ILI9341_GREEN); delay(80);
tft.fillScreen(ILI9341_BLUE); delay(80);
tft.fillScreen(ILI9341_BLACK);
showMessage("Serial: cal9 clearcal showcoef");
tft.setCursor(10,40);
tft.setTextSize(1);
tft.setTextColor(ILI9341_CYAN);
tft.println("Serial: irrecv irstop irtest irsend <hex> irsendname <Name>");
}
// ---------- Setup ----------
void setup() {
Serial.begin(115200);
delay(200);
Serial.println("\nStarting portrait affine-calibration test + IR...");
// SPI init with custom pins (ESP32)
SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI);
tft.begin();
// Setup I2C for FT6206 (SDA, SCL)
Wire.begin(I2C_SDA, I2C_SCL);
delay(10);
i2cScannerOnce();
// FT6206 init
if (!ctp.begin(40)) {
Serial.println("FT6206 not found at 40; trying default...");
if (!ctp.begin()) {
Serial.println("FT6206 not found. Touch disabled.");
tft.fillScreen(ILI9341_BLACK);
tft.setCursor(6,20);
tft.setTextSize(2);
tft.setTextColor(ILI9341_RED);
tft.println("FT6206 not found!");
} else Serial.println("FT6206 init (no arg).");
} else Serial.println("FT6206 initialized.");
// IR init
IrReceiver.begin(IR_RX_PIN, ENABLE_LED_FEEDBACK); // uses new IRRemote v3 API if available
// In case IRremote older API is used, also call irrecv.enableIRIn();
irrecv.enableIRIn(); // safe to call for compatibility
irsend.begin();
loadAffine();
tftIntro();
if (!affine_valid) {
delay(600);
Serial.println("No affine calibration found. Send 'cal9' to run it.");
}
Serial.println("Ready. Commands: cal9 clearcal showcoef irrecv irstop irtest irsend <hex> irsendname <Name>");
}
// ---------- Helpers to parse serial commands ----------
uint32_t parseHex(String s) {
s.trim();
if (s.startsWith("0x") || s.startsWith("0X")) s = s.substring(2);
uint32_t val = 0;
for (size_t i=0;i<s.length();i++){
char c = s[i];
val <<= 4;
if (c>='0' && c<='9') val |= (c - '0');
else if (c>='a' && c<='f') val |= (10 + (c - 'a'));
else if (c>='A' && c<='F') val |= (10 + (c - 'A'));
else break;
}
return val;
}
// ---------- Loop ----------
void loop() {
// Serial command handling
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n'); cmd.trim();
if (cmd.equalsIgnoreCase("cal9")) {
run9PointCalibration();
tftIntro();
return;
} else if (cmd.equalsIgnoreCase("clearcal")) {
clearAffine();
tftIntro();
return;
} else if (cmd.equalsIgnoreCase("showcoef")) {
if (affine_valid) {
Serial.printf("a=%.6f b=%.6f c=%.6f\n", a_coef,b_coef,c_coef);
Serial.printf("d=%.6f e=%.6f f=%.6f\n", d_coef,e_coef,f_coef);
} else Serial.println("No coeffs saved.");
} else if (cmd.equalsIgnoreCase("irrecv")) {
irPrintEnabled = true;
Serial.println("IR printing enabled.");
} else if (cmd.equalsIgnoreCase("irstop")) {
irPrintEnabled = false;
Serial.println("IR printing disabled.");
tft.fillRect(0,0, tft.width(), 48, ILI9341_BLACK);
} else if (cmd.equalsIgnoreCase("irtest")) {
if (lastIRValue != 0) {
const char* nm = lookupNameForCode(lastIRValue);
displayIROnTFT(lastIRValue, lastIRProto.c_str(), nm);
} else Serial.println("No IR code received yet.");
} else if (cmd.startsWith("irsend ")) {
String arg = cmd.substring(7); arg.trim();
uint32_t v = parseHex(arg);
if (v == 0) Serial.println("Parsed 0x0 (check hex).");
Serial.printf("Sending NEC 0x%08X\n", (unsigned)v);
// Send as NEC 32-bit
irsend.sendNEC(v, 32);
} else if (cmd.startsWith("irsendname ")) {
String name = cmd.substring(11); name.trim();
bool found=false;
for (int i=0;i<REMOTE_MAP_COUNT;i++){
if (name.equalsIgnoreCase(remoteMap[i].name)) {
Serial.printf("Sending named NEC %s = 0x%08X\n", remoteMap[i].name, (unsigned)remoteMap[i].code);
irsend.sendNEC(remoteMap[i].code, 32);
found=true;
break;
}
}
if (!found) Serial.println("Name not found in map. Use exact name from table.");
} else {
Serial.print("Unknown cmd: "); Serial.println(cmd);
}
}
// Touch handling
if (ctp.touched()) {
RawPt r;
TS_Point p = ctp.getPoint();
r.x = p.x; r.y = p.y;
Serial.print("RAW: "); Serial.print(r.x); Serial.print(","); Serial.println(r.y);
int sx, sy;
rawToScreen(r, sx, sy);
Serial.print("Screen: "); Serial.print(sx); Serial.print(","); Serial.println(sy);
tft.fillCircle(sx, sy, 4, ILI9341_YELLOW);
tft.fillRect(6, tft.height()-28, 200, 22, ILI9341_BLACK);
tft.setCursor(8, tft.height()-26);
tft.setTextSize(2);
tft.setTextColor(ILI9341_WHITE);
tft.print(sx); tft.print(","); tft.print(sy);
lastTouchTime = millis();
delay(40);
} else {
if (millis() - lastTouchTime > clearTimeout && lastTouchTime != 0) {
tft.fillRect(6, tft.height()-28, 200, 22, ILI9341_BLACK);
lastTouchTime = 0;
}
}
// IR receive handling (compatibility)
// Try both IRremote styles: IrReceiver (new) + irrecv.decode (old)
bool handledIR = false;
// Newer API: IrReceiver
if (IrReceiver.decode()) {
uint32_t value = IrReceiver.decodedIRData.decodedRawData;
// Many NEC decoders put 32-bit in raw, try fallback to command
value = IrReceiver.decodedIRData.command; // for NEC, this often returns command; keep as fallback
lastIRValue = IrReceiver.decodedIRData.command;
lastIRProto = "IrReceiver";
IrReceiver.resume();
handledIR = true;
}
// Old API: irrecv.decode
if (!handledIR) {
if (irrecv.decode(&irResults)) {
uint32_t value = (uint32_t)irResults.value;
lastIRValue = value;
lastIRTime = millis();
lastIRProto = (irResults.decode_type==NEC) ? "NEC" : "RAW";
const char* nm = lookupNameForCode(value);
Serial.print("IR recv: proto=");
Serial.print(lastIRProto);
Serial.print(" value=0x"); Serial.println(value, HEX);
if (nm) Serial.printf("Mapped name: %s\n", nm);
if (irPrintEnabled) {
displayIROnTFT(value, lastIRProto.c_str(), nm);
}
irrecv.resume();
}
}
// If using old decode API, we already resumed above. Nothing else to do.
delay(8);
}
Loading
esp32-c6-devkitc-1
esp32-c6-devkitc-1
Loading
ili9341-cap-touch
ili9341-cap-touch