/*
* Robot Traceur de Cartes IoT
* Adapté au montage Wokwi exact
* ENASTIC 2025-2026
*
* GPIO:
* 26 → LED bleue (mouvement X+)
* 27 → LED rouge (mouvement X-)
* 33 → LED rouge (mouvement Y+)
* 19 → LED verte (mouvement Y-)
* 17 → LED verte (stylo posé)
* 32 → Servo (stylo haut/bas)
* 35 → Bouton (reset position)
* 21 → LCD SDA
* 22 → LCD SCL
*/
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESP32Servo.h>
#include <ArduinoJson.h>
// ── WiFi Wokwi ────────────────────────────────────────────
const char* SSID = "Wokwi-GUEST";
const char* PASS = "";
// ── GPIO selon ton montage ────────────────────────────────
#define LED_X_POS 26 // LED bleue → X positif
#define LED_X_NEG 27 // LED rouge → X négatif
#define LED_Y_POS 33 // LED rouge → Y positif
#define LED_Y_NEG 19 // LED verte → Y négatif
#define LED_PEN 17 // LED verte → stylo posé
#define SERVO_PIN 32 // Servo → stylo
#define BTN_RESET 35 // Bouton → reset (INPUT only)
// ── LCD I2C (SDA=21, SCL=22) ──────────────────────────────
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ── Serveur HTTP port 80 ──────────────────────────────────
WebServer server(80);
// ── Servo ─────────────────────────────────────────────────
Servo penServo;
const int SERVO_UP = 90;
const int SERVO_DOWN = 30;
// ── État du robot ─────────────────────────────────────────
float posX = 0.0;
float posY = 0.0;
bool penIsDown = false;
bool isDrawing = false;
bool stopNow = false;
// ── Interface HTML complète ───────────────────────────────
const char HTML[] PROGMEM = R"html(
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Robot Traceur IoT</title>
<style>
:root {
--green: #00C853;
--dark: #121212;
--card: #1E1E1E;
--blue: #1565C0;
--orange: #E65100;
--red: #B71C1C;
--gray: #9E9E9E;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: var(--dark);
color: white;
padding: 16px;
min-height: 100vh;
}
h1 { color: var(--green); margin-bottom: 4px; font-size: 1.4rem; }
.sub { color: var(--gray); font-size: 0.82rem; margin-bottom: 14px; }
.status-bar {
display: flex; gap: 16px; flex-wrap: wrap;
background: #1A2A1A;
border: 1px solid var(--green);
border-radius: 10px;
padding: 10px 14px;
margin-bottom: 14px;
}
.si { display: flex; flex-direction: column; }
.sl { color: var(--gray); font-size: 0.72rem; }
.sv { color: var(--green); font-weight: 700; font-size: 1rem; }
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.card {
background: var(--card);
border-radius: 12px;
padding: 16px;
border: 1px solid #2A2A2A;
}
.card h2 { color: var(--green); font-size: 0.95rem; margin-bottom: 10px; }
button {
width: 100%; border: none; border-radius: 8px;
padding: 10px; font-size: 0.88rem; font-weight: 600;
cursor: pointer; margin: 3px 0; transition: opacity 0.2s;
}
button:hover { opacity: 0.85; }
.g { background: var(--green); color: #000; }
.b { background: var(--blue); color: #fff; }
.o { background: var(--orange);color: #fff; }
.r { background: var(--red); color: #fff; }
.outline {
background: transparent;
color: var(--green);
border: 1.5px solid var(--green);
}
.row { display: flex; gap: 8px; }
.row button { flex: 1; }
label { color: var(--gray); font-size: 0.75rem; display: block; margin-bottom: 2px; }
input[type="number"] {
background: #2A2A2A; color: white;
border: 1px solid #444; border-radius: 8px;
padding: 8px; width: 100%; font-size: 0.9rem; margin-bottom: 6px;
}
textarea {
width: 100%; background: #1A1A1A; color: #00FF88;
border: 1px solid #333; border-radius: 8px;
padding: 8px; font-family: monospace;
font-size: 0.78rem; resize: vertical; height: 110px;
}
.pb-wrap {
background: #333; border-radius: 99px;
height: 5px; margin: 8px 0; overflow: hidden;
}
.pb { background: var(--green); height: 100%; width: 0%; transition: width 0.4s; border-radius: 99px; }
#msg { color: var(--gray); font-size: 0.78rem; margin-top: 4px; min-height: 18px; }
.canvas-wrap { grid-column: 1 / -1; }
canvas { background: white; border-radius: 8px; display: block; margin: 0 auto; max-width: 100%; }
.canvas-btns { display: flex; gap: 8px; margin-top: 8px; }
.canvas-btns button { width: auto; padding: 6px 14px; }
@media(max-width:560px){ .grid{ grid-template-columns:1fr; } }
</style>
</head>
<body>
<h1>🤖 Robot Traceur de Cartes IoT</h1>
<p class="sub">Wokwi ESP32 — ENASTIC 2025-2026</p>
<div class="status-bar">
<div class="si"><span class="sl">Position X</span><span class="sv" id="sx">0.0 mm</span></div>
<div class="si"><span class="sl">Position Y</span><span class="sv" id="sy">0.0 mm</span></div>
<div class="si"><span class="sl">Stylo</span><span class="sv" id="spen">LEVÉ</span></div>
<div class="si"><span class="sl">Statut</span><span class="sv" id="sdraw">En attente</span></div>
<div class="si"><span class="sl">WiFi</span><span class="sv" id="swifi">...</span></div>
</div>
<div class="grid">
<!-- Contrôle manuel -->
<div class="card">
<h2>🎮 Contrôle Manuel</h2>
<button class="g" onclick="home()">⌂ Retour Origine (Home)</button>
<div style="margin-top:8px">
<label>X cible (mm)</label>
<input type="number" id="mx" value="0" step="10" min="0" max="200">
<label>Y cible (mm)</label>
<input type="number" id="my" value="0" step="10" min="0" max="200">
<button class="b" onclick="moveTo()">➡ Aller à X, Y</button>
</div>
<div class="row" style="margin-top:8px">
<button class="outline" onclick="penUp()">⬆ Stylo HAUT</button>
<button class="o" onclick="penDown()">⬇ Stylo BAS</button>
</div>
</div>
<!-- G-code -->
<div class="card">
<h2>📝 Commandes G-code</h2>
<label>Coller votre G-code ici :</label>
<textarea id="gc" placeholder="G0 Z5 G0 X0 Y0 G1 Z-1 G1 X50 Y0 G1 X50 Y50 G1 X0 Y50 G1 X0 Y0 G0 Z5 M2"></textarea>
<div class="pb-wrap"><div class="pb" id="pb"></div></div>
<div class="row">
<button class="g" style="flex:2" onclick="sendGcode()">▶ Exécuter</button>
<button class="r" onclick="stopDraw()">⏹ STOP</button>
</div>
<div id="msg"></div>
</div>
<!-- Canvas -->
<div class="card canvas-wrap">
<h2>🗺 Visualisation en temps réel</h2>
<canvas id="cv" width="580" height="320"></canvas>
<div class="canvas-btns">
<button class="outline" onclick="clearCv()">🗑 Effacer</button>
<button class="outline" onclick="saveCv()">💾 Sauvegarder PNG</button>
</div>
</div>
</div>
<script>
const BASE = ''; // même origine que l'ESP32
// ── Canvas ────────────────────────────────────────────────
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const SC = 2.4, OX = 40, OY = 290;
let lx = null, ly = null;
function r2c(rx, ry){ return { x: OX + rx*SC, y: OY - ry*SC }; }
function initCv(){
ctx.fillStyle = '#fff';
ctx.fillRect(0,0,cv.width,cv.height);
ctx.strokeStyle = '#DDD'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(OX,0); ctx.lineTo(OX,cv.height);
ctx.moveTo(0,OY); ctx.lineTo(cv.width,OY);
ctx.stroke();
ctx.fillStyle='#AAA'; ctx.font='11px Arial';
ctx.fillText('X →', cv.width-28, OY-4);
ctx.fillText('↑ Y', OX+4, 14);
ctx.fillText('0,0', OX+3, OY-3);
}
function updateCv(rx, ry, down){
const {x,y} = r2c(rx,ry);
if(down && lx!==null){
ctx.beginPath(); ctx.moveTo(lx,ly); ctx.lineTo(x,y);
ctx.strokeStyle='#1565C0'; ctx.lineWidth=1.8; ctx.stroke();
}
ctx.fillStyle = down ? '#E53935' : '#00C853';
ctx.beginPath(); ctx.arc(x,y,4,0,Math.PI*2); ctx.fill();
lx=x; ly=y;
}
function clearCv(){ lx=null; ly=null; initCv(); }
function saveCv(){
const a=document.createElement('a');
a.download='carte_robot.png'; a.href=cv.toDataURL(); a.click();
}
// ── API ───────────────────────────────────────────────────
async function get(r){
try{ const res=await fetch(BASE+r); return await res.json(); }
catch(e){ return null; }
}
async function post(r,b){
try{
const res=await fetch(BASE+r,{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify(b)
});
return await res.json();
} catch(e){ return null; }
}
async function home() { await post('/home',{}); clearCv(); }
async function penUp() { await post('/pen',{state:'up'}); }
async function penDown() { await post('/pen',{state:'down'}); }
async function stopDraw(){ await post('/stop',{}); document.getElementById('sdraw').textContent='Arrêté'; }
async function moveTo(){
const x=parseFloat(document.getElementById('mx').value)||0;
const y=parseFloat(document.getElementById('my').value)||0;
await post('/move',{x,y});
}
async function sendGcode(){
const gc=document.getElementById('gc').value.trim();
if(!gc){ setMsg('Aucun G-code à envoyer.'); return; }
setMsg('Envoi en cours...'); setPb(20);
try{
const r=await fetch(BASE+'/draw',{
method:'POST',
headers:{'Content-Type':'text/plain'},
body:gc
});
const d=await r.json();
setMsg('✓ Exécuté : '+d.lines+' lignes G-code');
setPb(100);
} catch(e){ setMsg('Erreur de connexion à l\'ESP32'); }
}
function setMsg(t){ document.getElementById('msg').textContent=t; }
function setPb(v){ document.getElementById('pb').style.width=v+'%'; }
// ── Statut (toutes les 800ms) ─────────────────────────────
async function refresh(){
const d=await get('/status');
if(!d){ document.getElementById('swifi').textContent='Déconnecté'; return; }
document.getElementById('sx').textContent = d.x.toFixed(1)+' mm';
document.getElementById('sy').textContent = d.y.toFixed(1)+' mm';
document.getElementById('spen').textContent = d.pen ? 'POSÉ' : 'LEVÉ';
document.getElementById('sdraw').textContent= d.drawing ? '⏳ En dessin…' : '✅ Prêt';
document.getElementById('swifi').textContent= 'Connecté';
updateCv(d.x, d.y, d.pen);
}
initCv();
setInterval(refresh, 800);
refresh();
</script>
</body>
</html>
)html";
// =============================================================
// FONCTIONS ROBOT
// =============================================================
void lcdShow(String l1, String l2 = ""){
lcd.clear();
lcd.setCursor(0,0); lcd.print(l1.substring(0,16));
if(l2.length()>0){ lcd.setCursor(0,1); lcd.print(l2.substring(0,16)); }
}
void penUp_f(){
penServo.write(SERVO_UP);
digitalWrite(LED_PEN, LOW);
penIsDown = false;
delay(120);
}
void penDown_f(){
penServo.write(SERVO_DOWN);
digitalWrite(LED_PEN, HIGH);
penIsDown = true;
delay(120);
}
// Clignote la LED de l'axe pour simuler le mouvement
void blinkAxis(int pin, int times){
for(int i=0;i<times;i++){
digitalWrite(pin, HIGH); delay(18);
digitalWrite(pin, LOW); delay(8);
}
}
void gotoXY(float tx, float ty){
float dx = tx - posX;
float dy = ty - posY;
// Simuler mouvement X
if(dx > 0) blinkAxis(LED_X_POS, min((int)abs(dx)/5+1, 8));
else if(dx < 0) blinkAxis(LED_X_NEG, min((int)abs(dx)/5+1, 8));
// Simuler mouvement Y
if(dy > 0) blinkAxis(LED_Y_POS, min((int)abs(dy)/5+1, 8));
else if(dy < 0) blinkAxis(LED_Y_NEG, min((int)abs(dy)/5+1, 8));
posX = tx; posY = ty;
// LCD : afficher position
String l1 = "X:"+String(posX,1)+" Y:"+String(posY,1);
String l2 = penIsDown ? "Stylo: BAS " : "Stylo: HAUT ";
l2 += isDrawing ? "DRW" : " ";
lcdShow(l1, l2);
Serial.print("POS X="); Serial.print(posX);
Serial.print(" Y="); Serial.println(posY);
}
void goHome(){
penUp_f();
gotoXY(0.0, 0.0);
lcdShow("HOME", "Position: 0,0");
Serial.println("HOME");
}
// =============================================================
// INTERPRÉTEUR G-CODE
// =============================================================
void executeGcode(const String& gcode){
isDrawing = true;
stopNow = false;
int debut = 0;
int totalLines = 0;
for(char c : gcode) if(c=='\n') totalLines++;
int doneLines = 0;
Serial.println("=== Début G-code ===");
while(debut < (int)gcode.length() && !stopNow){
int fin = gcode.indexOf('\n', debut);
if(fin < 0) fin = gcode.length();
String line = gcode.substring(debut, fin);
line.trim();
debut = fin + 1;
if(line.length()==0 || line[0]==';') continue;
Serial.println(line);
doneLines++;
if(line.startsWith("G0") || line.startsWith("G1")){
float nx = posX, ny = posY;
int xi = line.indexOf('X');
int yi = line.indexOf('Y');
int zi = line.indexOf('Z');
if(xi >= 0) nx = line.substring(xi+1).toFloat();
if(yi >= 0) ny = line.substring(yi+1).toFloat();
if(zi >= 0){
float z = line.substring(zi+1).toFloat();
if(z >= 0) penUp_f(); else penDown_f();
}
if(line.startsWith("G0")) penUp_f();
gotoXY(nx, ny);
} else if(line.startsWith("G28")){
goHome();
} else if(line.startsWith("M2") || line.startsWith("M30")){
break;
}
server.handleClient(); // Garder le serveur réactif
}
penUp_f();
isDrawing = false;
lcdShow("Terminé!", "X:"+String(posX,1)+" Y:"+String(posY,1));
Serial.println("=== G-code terminé ===");
}
// =============================================================
// ROUTES HTTP
// =============================================================
void rRoot(){
server.send(200, "text/html; charset=utf-8", HTML);
}
void rStatus(){
StaticJsonDocument<128> doc;
doc["x"] = posX;
doc["y"] = posY;
doc["pen"] = penIsDown;
doc["drawing"] = isDrawing;
String out; serializeJson(doc, out);
server.send(200, "application/json", out);
}
void rMove(){
if(!server.hasArg("plain")){
server.send(400,"application/json","{\"err\":\"no body\"}");
return;
}
StaticJsonDocument<64> doc;
deserializeJson(doc, server.arg("plain"));
gotoXY(doc["x"]|posX, doc["y"]|posY);
server.send(200,"application/json","{\"ok\":true}");
}
void rPen(){
if(!server.hasArg("plain")){
server.send(400,"application/json","{\"err\":\"no body\"}");
return;
}
StaticJsonDocument<32> doc;
deserializeJson(doc, server.arg("plain"));
String s = doc["state"]|"up";
if(s=="down") penDown_f(); else penUp_f();
server.send(200,"application/json","{\"ok\":true}");
}
void rHome(){
goHome();
server.send(200,"application/json","{\"ok\":true}");
}
void rStop(){
stopNow = true;
isDrawing = false;
penUp_f();
server.send(200,"application/json","{\"ok\":true,\"stopped\":true}");
}
void rDraw(){
if(!server.hasArg("plain")){
server.send(400,"application/json","{\"err\":\"no gcode\"}");
return;
}
String gcode = server.arg("plain");
int nb = 0;
for(char c : gcode) if(c=='\n') nb++;
server.send(200,"application/json",
"{\"ok\":true,\"lines\":"+String(nb)+"}");
executeGcode(gcode);
}
// =============================================================
// SETUP & LOOP
// =============================================================
// ── AUTOTEST DE DÉMONSTRATION ─────────────────────────────
// S'exécute au démarrage pour prouver que tout fonctionne
void autoTest() {
Serial.println("=== AUTOTEST DÉMARRAGE ===");
// 1. Annonce sur LCD
lcdShow("AUTOTEST...", "Initialisation");
delay(800);
// 2. Test du stylo (servo)
lcdShow("Test stylo", "Stylo BAS");
Serial.println("[1] Stylo BAS");
penDown_f();
delay(600);
lcdShow("Test stylo", "Stylo HAUT");
Serial.println("[2] Stylo HAUT");
penUp_f();
delay(600);
// 3. Test axe X (LED bleue = X+, LED rouge = X-)
lcdShow("Test Axe X", "X+ -> X-");
Serial.println("[3] Axe X : X+ puis X-");
// X positif : 3 clignotements
for (int i = 0; i < 3; i++) {
digitalWrite(LED_X_POS, HIGH);
delay(200);
digitalWrite(LED_X_POS, LOW);
delay(150);
}
delay(200);
// X négatif : 3 clignotements
for (int i = 0; i < 3; i++) {
digitalWrite(LED_X_NEG, HIGH);
delay(200);
digitalWrite(LED_X_NEG, LOW);
delay(150);
}
delay(300);
// 4. Test axe Y (LED rouge = Y+, LED verte = Y-)
lcdShow("Test Axe Y", "Y+ -> Y-");
Serial.println("[4] Axe Y : Y+ puis Y-");
for (int i = 0; i < 3; i++) {
digitalWrite(LED_Y_POS, HIGH);
delay(200);
digitalWrite(LED_Y_POS, LOW);
delay(150);
}
delay(200);
for (int i = 0; i < 3; i++) {
digitalWrite(LED_Y_NEG, HIGH);
delay(200);
digitalWrite(LED_Y_NEG, LOW);
delay(150);
}
delay(300);
// 5. Simulation complète G-code : tracer un carré
lcdShow("Simulation", "Trace carre...");
Serial.println("[5] Simulation G-code : carre 50x50mm");
String gcodeTest =
"G21\n"
"G90\n"
"G28\n"
"G0 Z5\n"
"G0 X0 Y0\n"
"G1 Z-1\n"
"G1 X50 Y0\n"
"G1 X50 Y50\n"
"G1 X0 Y50\n"
"G1 X0 Y0\n"
"G0 Z5\n"
"M2\n";
executeGcode(gcodeTest);
delay(500);
// 6. Fin de l'autotest
Serial.println("[6] Connectivité WiFi : OK");
Serial.print(" IP du robot : ");
Serial.println(WiFi.localIP());
Serial.println("[7] Serveur HTTP : Port 80 actif");
Serial.println("[8] Routes disponibles :");
Serial.println(" GET /status");
Serial.println(" POST /move {x, y}");
Serial.println(" POST /pen {state: up|down}");
Serial.println(" POST /home");
Serial.println(" POST /draw <gcode>");
Serial.println("=== AUTOTEST TERMINÉ : SYSTÈME OK ===");
// Affichage final LCD
lcdShow("SYSTEME OK !", "IP:" + WiFi.localIP().toString());
delay(1000);
lcdShow("Robot pret!", "Port 80 actif");
}
void setup(){
Serial.begin(115200);
Serial.println("\n=== Robot Traceur IoT — Wokwi ===");
// GPIO
pinMode(LED_X_POS, OUTPUT);
pinMode(LED_X_NEG, OUTPUT);
pinMode(LED_Y_POS, OUTPUT);
pinMode(LED_Y_NEG, OUTPUT);
pinMode(LED_PEN, OUTPUT);
// GPIO35 = input only sur ESP32, pas de pullup interne
pinMode(BTN_RESET, INPUT);
// Servo
penServo.attach(SERVO_PIN);
penUp_f();
// LCD
Wire.begin(21, 22);
lcd.init();
lcd.backlight();
lcdShow("Robot Traceur", "Connexion WiFi");
// WiFi
WiFi.begin(SSID, PASS);
Serial.print("Connexion");
int n = 0;
while(WiFi.status()!=WL_CONNECTED && n<30){
delay(500); Serial.print("."); n++;
}
if(WiFi.status()==WL_CONNECTED){
Serial.println("\nConnecté !");
Serial.print("IP : ");
Serial.println(WiFi.localIP());
lcdShow("IP:", WiFi.localIP().toString());
} else {
Serial.println("\nErreur WiFi");
lcdShow("Erreur WiFi", "Verif SSID");
}
// Routes
server.on("/", HTTP_GET, rRoot);
server.on("/status", HTTP_GET, rStatus);
server.on("/move", HTTP_POST, rMove);
server.on("/pen", HTTP_POST, rPen);
server.on("/home", HTTP_POST, rHome);
server.on("/stop", HTTP_POST, rStop);
server.on("/draw", HTTP_POST, rDraw);
server.begin();
Serial.println("Serveur HTTP démarré !");
lcdShow("Robot pret!", "Port 80 actif");
}
void loop(){
server.handleClient();
// Bouton reset (GPIO35 = input only, pas de pullup)
if(digitalRead(BTN_RESET)==HIGH){
delay(50);
if(digitalRead(BTN_RESET)==HIGH){
goHome();
delay(600);
}
}
delay(1);
}