firmware: v3 — nuevo diseño web HUD

- Interfaz completamente rediseñada: tema militar/racing oscuro
- Fondo con grid animado, tipografia Orbitron
- Barras de potencia estilo LED (6 segmentos verde→rojo)
- Botones angulares con glow rojo (traccion) y azul (camara)
- Flechas CSS puras, crosshair en centro de camara
- Esquinas HUD animadas sobre el stream
- camera_index.h regenerado desde data/index.html (4.6KB gzipped)
- gen_header.py para regenerar el header en el futuro

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Natxo1000
2026-06-02 11:54:11 +02:00
parent 830b8fb579
commit 8a93578c9f
8 changed files with 574 additions and 1249 deletions

View File

@@ -10,7 +10,7 @@
// ================================================================
// OTA CONFIG
// ================================================================
#define FW_VERSION 2
#define FW_VERSION 3
#define GITEA_HOST "https://git.nacho.myds.me"
#define GITEA_OWNER "Natxo"
#define GITEA_REPO "Arduino-Car"

View File

@@ -612,19 +612,7 @@ static esp_err_t win_handler(httpd_req_t *req) {
static esp_err_t index_handler(httpd_req_t *req) {
httpd_resp_set_type(req, "text/html");
httpd_resp_set_hdr(req, "Content-Encoding", "gzip");
sensor_t *s = esp_camera_sensor_get();
if (s != NULL) {
if (s->id.PID == OV3660_PID) {
return httpd_resp_send(req, (const char *)index_ov3660_html_gz, index_ov3660_html_gz_len);
} else if (s->id.PID == OV5640_PID) {
return httpd_resp_send(req, (const char *)index_ov5640_html_gz, index_ov5640_html_gz_len);
} else {
return httpd_resp_send(req, (const char *)index_ov2640_html_gz, index_ov2640_html_gz_len);
}
} else {
log_e("Camera sensor not found");
return httpd_resp_send_500(req);
}
return httpd_resp_send(req, (const char *)index_html_gz, index_html_gz_len);
}
void startCameraServer() {

File diff suppressed because it is too large Load Diff

View File

@@ -3,458 +3,324 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Coche RC</title>
<title>COCHE RC</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&display=swap" rel="stylesheet">
<style>
:root {
--red: #e63946;
--blue: #4361ee;
--orange: #f4a261;
--bg: #08080f;
--panel: rgba(10, 10, 20, 0.82);
--btn: rgba(30, 30, 50, 0.9);
--text: #e0e0e0;
--dim: #666;
--border: rgba(255,255,255,0.07);
--red: #e8001e;
--red-g: rgba(232,0,30,.55);
--blue: #1a8cff;
--blue-g: rgba(26,140,255,.45);
--cyan: #00ccff;
--green: #00ff88;
--bg: #04060e;
--text: #c8d6e8;
--dim: rgba(160,185,210,.4);
--grid: rgba(0,170,255,.04);
--border: rgba(0,170,255,.14);
}
*, *::before, *::after {
margin: 0; padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
margin:0; padding:0; box-sizing:border-box;
-webkit-tap-highlight-color:transparent; user-select:none;
}
html, body {
width: 100%; height: 100%;
overflow: hidden;
background: var(--bg);
color: var(--text);
font-family: 'Segoe UI', system-ui, sans-serif;
width:100%; height:100%; overflow:hidden;
background:var(--bg); color:var(--text);
font-family:'Orbitron','Courier New',monospace;
}
/* ── STATUS BAR ─────────────────────────────────── */
/* GRID BG */
body::before {
content:''; position:fixed; inset:0; z-index:0; pointer-events:none;
background:
repeating-linear-gradient(90deg,transparent 0,transparent 49px,var(--grid) 49px,var(--grid) 50px),
repeating-linear-gradient(0deg, transparent 0,transparent 49px,var(--grid) 49px,var(--grid) 50px);
animation:gridMove 25s linear infinite;
}
@keyframes gridMove { to { background-position:0 50px; } }
/* TOP BAR */
#bar {
position: fixed;
top: 0; left: 0; right: 0;
height: 38px;
background: rgba(5,5,15,0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 14px;
gap: 10px;
z-index: 200;
position:fixed; top:0; left:0; right:0; height:46px;
background:rgba(4,6,16,.92); border-bottom:1px solid var(--border);
backdrop-filter:blur(14px);
display:flex; align-items:center; padding:0 16px; gap:12px; z-index:200;
}
.bar-title {
font-size: 13px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--red);
.bar-logo {
font-size:14px; font-weight:900; letter-spacing:5px;
color:var(--red); text-shadow:0 0 14px var(--red),0 0 30px rgba(232,0,30,.3);
}
.bar-div { width:1px; height:22px; background:var(--border); }
.bar-dot { width:9px; height:9px; border-radius:50%; background:#222; flex-shrink:0; transition:all .4s; }
.bar-dot.live { background:var(--green); box-shadow:0 0 10px var(--green); animation:pulse 2.4s ease-in-out infinite; }
.bar-dot.err { background:var(--red); box-shadow:0 0 10px var(--red); animation:blink .9s infinite; }
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:.4} }
@keyframes blink { 50%{opacity:.15} }
#bar-status { font-size:9px; letter-spacing:2px; color:var(--dim); }
.bar-right { margin-left:auto; display:flex; align-items:center; gap:14px; }
#bar-speed { font-size:10px; letter-spacing:3px; color:var(--cyan); }
.bar-fw { font-size:9px; letter-spacing:2px; color:rgba(0,204,255,.3); }
.dot {
width: 8px; height: 8px;
border-radius: 50%;
background: #333;
flex-shrink: 0;
transition: background .4s, box-shadow .4s;
}
.dot.ok { background: #2ecc71; box-shadow: 0 0 7px #2ecc71; }
.dot.err { background: var(--red); box-shadow: 0 0 7px var(--red); animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: .3; } }
#bar-status { font-size: 11px; color: var(--dim); }
#bar-speed { margin-left: auto; font-size: 12px; font-weight: 600; color: var(--orange); letter-spacing: 1px; }
/* ── VIDEO ──────────────────────────────────────── */
/* VIDEO */
#screen {
position: fixed;
top: 38px; left: 0; right: 0; bottom: 0;
background: #000;
display: flex;
align-items: center;
justify-content: center;
position:fixed; top:46px; left:0; right:0; bottom:0;
background:#000; display:flex; align-items:center; justify-content:center; z-index:1;
}
#stream { max-width:100%; max-height:100%; object-fit:contain; display:block; }
#stream {
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
/* HUD corners */
.hc {
position:absolute; width:32px; height:32px;
border-color:var(--cyan); border-style:solid;
opacity:.4; z-index:3; pointer-events:none; transition:opacity .5s;
}
#hc-tl { top:10px; left:10px; border-width:2px 0 0 2px; }
#hc-tr { top:10px; right:10px; border-width:2px 2px 0 0; }
#hc-bl { bottom:195px;left:10px; border-width:0 0 2px 2px; }
#hc-br { bottom:195px;right:10px; border-width:0 2px 2px 0; }
#screen.live .hc { opacity:.7; }
/* ── CONTROLS OVERLAY ───────────────────────────── */
#controls {
position: fixed;
bottom: 0; left: 0; right: 0;
/* CONTROLS OVERLAY */
#ctrl {
position:fixed; bottom:0; left:0; right:0;
padding:0 12px 14px;
background: linear-gradient(transparent 0%, rgba(5,5,15,0.88) 45%);
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 8px;
z-index: 100;
pointer-events: none; /* let video clicks through the gradient */
background:linear-gradient(transparent,rgba(4,6,14,.97) 36%);
display:flex; align-items:flex-end; justify-content:space-between; gap:8px; z-index:100;
}
#controls > * { pointer-events: all; }
/* ── D-PAD ──────────────────────────────────────── */
.dpad {
display: grid;
grid-template-columns: repeat(3, 54px);
grid-template-rows: repeat(3, 54px);
gap: 5px;
}
.dpad.small {
grid-template-columns: repeat(3, 44px);
grid-template-rows: repeat(3, 44px);
gap: 4px;
}
/* D-PAD */
.pad-wrap { display:flex; flex-direction:column; align-items:center; gap:5px; }
.pad-lbl { font-size:7px; letter-spacing:4px; color:var(--dim); margin-bottom:1px; }
.dpad { display:grid; grid-template-columns:repeat(3,54px); grid-template-rows:repeat(3,54px); gap:4px; }
.dpad.sm { grid-template-columns:repeat(3,44px); grid-template-rows:repeat(3,44px); }
.btn {
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
background: var(--btn);
border: 1px solid var(--border);
color: var(--text);
font-size: 22px;
cursor: pointer;
transition: transform .08s, background .08s, box-shadow .08s;
touch-action: none;
backdrop-filter: blur(6px);
display:flex; align-items:center; justify-content:center;
background:rgba(6,10,22,.96); cursor:pointer;
transition:transform .07s,background .07s,box-shadow .07s;
touch-action:none; position:relative; overflow:hidden;
}
.btn::after { content:''; position:absolute; bottom:0; right:0; border-style:solid; border-width:0 0 9px 9px; }
/* car buttons — red accent */
.btn.car.pressed, .btn.car:active {
background: var(--red);
transform: scale(0.9);
box-shadow: 0 0 16px rgba(230,57,70,.55);
}
.btn.car { border:1px solid rgba(232,0,30,.3); }
.btn.car::after { border-color:transparent transparent rgba(232,0,30,.45) transparent; }
.btn.car.on,.btn.car:active { background:rgba(232,0,30,.18); box-shadow:0 0 18px var(--red-g),inset 0 0 10px rgba(232,0,30,.12); transform:scale(.9); }
/* camera buttons — blue accent */
.btn.cam { font-size: 18px; border-radius: 8px; }
.btn.cam.pressed, .btn.cam:active {
background: var(--blue);
transform: scale(0.9);
box-shadow: 0 0 14px rgba(67,97,238,.55);
}
.btn.cam.center { font-size: 14px; }
.btn.cam { border:1px solid rgba(26,140,255,.25); }
.btn.cam::after { border-color:transparent transparent rgba(26,140,255,.35) transparent; }
.btn.cam.on,.btn.cam:active { background:rgba(26,140,255,.18); box-shadow:0 0 16px var(--blue-g),inset 0 0 10px rgba(26,140,255,.12); transform:scale(.9); }
/* ── CENTER PANEL ───────────────────────────────── */
.center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
max-width: 180px;
}
/* CSS arrows */
.ar-u { width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent; }
.ar-d { width:0;height:0;border-left:10px solid transparent;border-right:10px solid transparent; }
.ar-l { width:0;height:0;border-top:10px solid transparent;border-bottom:10px solid transparent; }
.ar-r { width:0;height:0;border-top:10px solid transparent;border-bottom:10px solid transparent; }
.btn.car .ar-u { border-bottom:16px solid var(--red); }
.btn.car .ar-d { border-top:16px solid var(--red); }
.btn.car .ar-l { border-right:16px solid var(--red); }
.btn.car .ar-r { border-left:16px solid var(--red); }
.btn.cam .ar-u { border-bottom:13px solid var(--blue); }
.btn.cam .ar-d { border-top:13px solid var(--blue); }
.btn.cam .ar-l { border-right:13px solid var(--blue); }
.btn.cam .ar-r { border-left:13px solid var(--blue); }
/* Speed slider */
.speed-wrap { width: 100%; display: flex; flex-direction: column; align-items: center; gap: 5px; }
.speed-label { font-size: 10px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--dim); }
/* Crosshair */
.xhair { position:relative; width:18px; height:18px; color:var(--cyan); }
.xhair::before { content:''; position:absolute; top:50%; left:2px; right:2px; height:1px; background:currentColor; transform:translateY(-50%); }
.xhair::after { content:''; position:absolute; left:50%; top:2px; bottom:2px; width:1px; background:currentColor; transform:translateX(-50%); }
/* CENTER */
.center { flex:1; display:flex; flex-direction:column; align-items:center; gap:10px; max-width:175px; }
/* Speed segments */
.spd-lbl { font-size:7px; letter-spacing:4px; color:var(--dim); text-align:center; margin-bottom:7px; }
.spd-segs { display:flex; gap:3px; width:100%; }
.seg { flex:1; height:22px; background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.07); transition:background .18s,box-shadow .18s; }
.seg[data-i="1"].lit { background:#00ff88; box-shadow:0 0 8px #00ff88; }
.seg[data-i="2"].lit { background:#55ff55; box-shadow:0 0 8px #55ff55; }
.seg[data-i="3"].lit { background:#aaee00; box-shadow:0 0 8px #aaee00; }
.seg[data-i="4"].lit { background:#ffcc00; box-shadow:0 0 8px #ffcc00; }
.seg[data-i="5"].lit { background:#ff7700; box-shadow:0 0 8px #ff7700; }
.seg[data-i="6"].lit { background:#e8001e; box-shadow:0 0 10px #e8001e; }
#speed {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 5px;
border-radius: 3px;
background: linear-gradient(to right, var(--red) 0%, var(--orange) var(--pct,40%), #222 var(--pct,40%));
outline: none;
cursor: pointer;
}
#speed::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px; height: 20px;
border-radius: 50%;
background: #fff;
box-shadow: 0 0 8px rgba(230,57,70,.6);
cursor: pointer;
width:100%; -webkit-appearance:none; appearance:none;
height:4px; background:transparent; cursor:pointer; margin-top:5px; outline:none;
}
#speed::-webkit-slider-runnable-track { background:rgba(0,200,255,.12); height:4px; }
#speed::-webkit-slider-thumb { -webkit-appearance:none; width:14px; height:14px; background:var(--cyan); box-shadow:0 0 8px var(--cyan); cursor:pointer; border-radius:0; }
/* Extras */
/* Extra buttons */
.extras { display:flex; gap:8px; }
.btn-extra {
padding: 8px 14px;
border-radius: 8px;
background: var(--btn);
border: 1px solid var(--border);
color: var(--text);
font-size: 18px;
cursor: pointer;
backdrop-filter: blur(6px);
transition: background .1s, box-shadow .1s, transform .08s;
}
.btn-extra:active { transform: scale(0.92); }
.btn-extra.active {
background: var(--orange);
box-shadow: 0 0 12px rgba(244,162,97,.5);
.btn-x {
padding:9px 15px; background:rgba(6,10,22,.96);
border:1px solid var(--border); color:var(--text);
font-family:inherit; font-size:17px; cursor:pointer;
transition:background .1s,box-shadow .1s,transform .07s; position:relative; overflow:hidden;
}
.btn-x::after { content:''; position:absolute; bottom:0; right:0; border-style:solid; border-width:0 0 7px 7px; border-color:transparent transparent var(--border) transparent; }
.btn-x:active { transform:scale(.91); }
.btn-x.on { background:rgba(232,120,0,.2); border-color:#f48020; box-shadow:0 0 14px rgba(244,128,32,.45); }
/* Keys hint */
.hint { font-size: 9px; color: rgba(255,255,255,.2); text-align: center; line-height: 1.8; }
.hint { font-size:7px; letter-spacing:2px; color:rgba(160,185,210,.18); text-align:center; line-height:2.2; }
/* ── LABELS ─────────────────────────────────────── */
.dpad-label {
font-size: 9px;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--dim);
text-align: center;
margin-bottom: 2px;
}
.dpad-wrap { display: flex; flex-direction: column; align-items: center; }
/* ── LANDSCAPE / SMALL SCREEN ───────────────────── */
/* RESPONSIVE */
@media (max-height:480px) {
.dpad { grid-template-columns: repeat(3, 44px); grid-template-rows: repeat(3, 44px); gap: 4px; }
.dpad.small { grid-template-columns: repeat(3, 36px); grid-template-rows: repeat(3, 36px); }
.btn { font-size: 18px; }
.btn.cam { font-size: 14px; }
#controls { padding: 0 10px 10px; }
.center { gap: 7px; max-width: 150px; }
.dpad { grid-template-columns:repeat(3,44px); grid-template-rows:repeat(3,44px); }
.dpad.sm { grid-template-columns:repeat(3,36px); grid-template-rows:repeat(3,36px); }
.seg { height:17px; }
#ctrl { padding:0 10px 10px; }
#hc-bl,#hc-br { bottom:165px; }
}
@media (max-width:380px) {
.dpad { grid-template-columns:repeat(3,48px); grid-template-rows:repeat(3,48px); }
.dpad.sm { grid-template-columns:repeat(3,40px); grid-template-rows:repeat(3,40px); }
}
</style>
</head>
<body>
<!-- STATUS BAR -->
<div class="hc" id="hc-tl"></div>
<div class="hc" id="hc-tr"></div>
<div class="hc" id="hc-bl"></div>
<div class="hc" id="hc-br"></div>
<div id="bar">
<span class="bar-title">RC</span>
<div class="dot" id="dot"></div>
<span id="bar-status">Conectando…</span>
<span id="bar-speed">VEL 3/6</span>
<span class="bar-logo">COCHE&nbsp;&nbsp;RC</span>
<div class="bar-div"></div>
<div class="bar-dot" id="dot"></div>
<span id="bar-status">CONECTANDO</span>
<div class="bar-right">
<span id="bar-speed">— / 6</span>
<span class="bar-fw">FW&nbsp;v2</span>
</div>
</div>
<!-- STREAM -->
<div id="screen">
<img id="stream" alt="Cámara ESP32">
<img id="stream" alt="">
</div>
<!-- CONTROLS -->
<div id="controls">
<div id="ctrl">
<!-- Coche -->
<div class="dpad-wrap">
<div class="dpad-label">Coche</div>
<div class="dpad" id="dpad-car">
<div></div>
<div class="btn car" data-cmd="F"></div>
<div></div>
<div class="btn car" data-cmd="L"></div>
<div></div>
<div class="btn car" data-cmd="R"></div>
<div></div>
<div class="btn car" data-cmd="B"></div>
<div class="pad-wrap">
<span class="pad-lbl">TRACCIÓN</span>
<div class="dpad">
<div></div><div class="btn car" id="b-F"><div class="ar-u"></div></div><div></div>
<div class="btn car" id="b-L"><div class="ar-l"></div></div>
<div></div>
<div class="btn car" id="b-R"><div class="ar-r"></div></div>
<div></div><div class="btn car" id="b-B"><div class="ar-d"></div></div><div></div>
</div>
</div>
<!-- Centro -->
<div class="center">
<div class="speed-wrap">
<span class="speed-label">Velocidad</span>
<div style="width:100%">
<div class="spd-lbl">POTENCIA</div>
<div class="spd-segs" id="segs">
<div class="seg" data-i="1"></div>
<div class="seg" data-i="2"></div>
<div class="seg" data-i="3"></div>
<div class="seg" data-i="4"></div>
<div class="seg" data-i="5"></div>
<div class="seg" data-i="6"></div>
</div>
<input type="range" id="speed" min="1" max="6" step="1" value="3">
</div>
<div class="extras">
<button class="btn-extra" id="btn-horn" title="Manten pulsado">📯</button>
<button class="btn-extra" id="btn-led" title="Luz">💡</button>
<button class="btn-x" id="b-horn">&#128239;</button>
<button class="btn-x" id="b-led">&#128161;</button>
</div>
<div class="hint">WASD · ←→↑↓ · E · F · R</div>
<div class="hint">W A S D &nbsp;·&nbsp; FLECHAS &nbsp;·&nbsp; E &nbsp;F &nbsp;R</div>
</div>
<!-- Cámara -->
<div class="dpad-wrap">
<div class="dpad-label">Cámara</div>
<div class="dpad small" id="dpad-cam">
<div></div>
<div class="btn cam" data-cam="YU"></div>
<div></div>
<div class="btn cam" data-cam="XL"></div>
<div class="btn cam center" data-cam="C"></div>
<div class="btn cam" data-cam="XR"></div>
<div></div>
<div class="btn cam" data-cam="YD"></div>
<div></div>
<div class="pad-wrap">
<span class="pad-lbl">CÁMARA</span>
<div class="dpad sm">
<div></div><div class="btn cam" id="b-YU"><div class="ar-u"></div></div><div></div>
<div class="btn cam" id="b-XL"><div class="ar-l"></div></div>
<div class="btn cam" id="b-C"><div class="xhair"></div></div>
<div class="btn cam" id="b-XR"><div class="ar-r"></div></div>
<div></div><div class="btn cam" id="b-YD"><div class="ar-d"></div></div><div></div>
</div>
</div>
</div>
<script>
// ── URLs ────────────────────────────────────────────────────────
const baseHost = document.location.origin;
const hostname = window.location.hostname;
const isHttps = baseHost.startsWith('https');
const streamSrc = isHttps
? `https://stream${hostname}/stream`
: `${baseHost.replace(/:\d+$/, '')}:81/stream`;
const base=document.location.origin,host=window.location.hostname;
const strmSrc=base.startsWith('https')?`https://stream${host}/stream`:`${base.replace(/:\d+$/,'')}:81/stream`;
const view=document.getElementById('stream'),screen=document.getElementById('screen');
const dot=document.getElementById('dot'),stLbl=document.getElementById('bar-status');
// ── STREAM con reconexión automática ────────────────────────────
const view = document.getElementById('stream');
const dot = document.getElementById('dot');
const barStatus = document.getElementById('bar-status');
function startStream() {
view.src = streamSrc + '?t=' + Date.now();
}
view.onload = () => {
dot.className = 'dot ok';
barStatus.textContent = 'En vivo';
};
view.onerror = () => {
dot.className = 'dot err';
barStatus.textContent = 'Reconectando…';
setTimeout(startStream, 2500);
};
function startStream(){view.src=strmSrc+'?t='+Date.now();}
view.onload =()=>{dot.className='bar-dot live';stLbl.textContent='EN VIVO';screen.classList.add('live');};
view.onerror=()=>{dot.className='bar-dot err';stLbl.textContent='RECONECTANDO';screen.classList.remove('live');setTimeout(startStream,2500);};
startStream();
// ── COMANDOS con cancelación de movimiento ───────────────────────
let moveCtrl = null; // AbortController para F/B/L/R/S
let ledOn = false;
let hornOn = false;
function sendCommand(cmd) {
let query;
switch (cmd) {
case 'K':
ledOn = !ledOn;
document.getElementById('btn-led').classList.toggle('active', ledOn);
query = `${baseHost}/control?var=led_intensity&val=${ledOn ? 255 : 0}`;
fetch(query).catch(() => {});
return;
default:
query = `${baseHost}/control?var=car&val=${cmd}`;
let mvCtrl=null,ledOn=false,hornOn=false;
function send(cmd){
let q;
if(cmd==='K'){ledOn=!ledOn;document.getElementById('b-led').classList.toggle('on',ledOn);fetch(`${base}/control?var=led_intensity&val=${ledOn?255:0}`).catch(()=>{});return;}
q=`${base}/control?var=car&val=${cmd}`;
if('FBLRS'.includes(cmd)&&cmd.length===1){if(mvCtrl)mvCtrl.abort();mvCtrl=new AbortController();fetch(q,{signal:mvCtrl.signal}).catch(()=>{});}
else fetch(q).catch(()=>{});
}
// Cancelar comando de movimiento anterior para no acumular cola
if (['F','B','L','R','S'].includes(cmd)) {
if (moveCtrl) moveCtrl.abort();
moveCtrl = new AbortController();
fetch(query, { signal: moveCtrl.signal }).catch(() => {});
} else {
fetch(query).catch(() => {});
}
}
const spEl=document.getElementById('speed'),segs=document.querySelectorAll('.seg'),spLbl=document.getElementById('bar-speed');
function applySpeed(){const v=+spEl.value;spLbl.textContent=`PWR ${v} / 6`;segs.forEach(s=>s.classList.toggle('lit',+s.dataset.i<=v));send('V'+Math.round(150+(v-1)*21));}
spEl.addEventListener('input',applySpeed);applySpeed();
// ── VELOCIDAD ───────────────────────────────────────────────────
const speedEl = document.getElementById('speed');
const barSpeed = document.getElementById('bar-speed');
let pX=90,pY=90;const ST=10;
function applySpeed() {
const level = parseInt(speedEl.value);
const pct = ((level - 1) / 5 * 100).toFixed(0);
speedEl.style.setProperty('--pct', pct + '%');
barSpeed.textContent = `VEL ${level}/6`;
sendCommand('V' + Math.round(150 + (level - 1) * 21));
}
speedEl.addEventListener('input', applySpeed);
applySpeed();
// ── SERVOS ──────────────────────────────────────────────────────
let posX = 90, posY = 90;
const STEP = 10;
function moveServo(axis, dir) {
if (axis === 'X') {
posX = Math.min(180, Math.max(0, posX + dir * STEP));
sendCommand('X' + posX);
} else {
posY = Math.min(180, Math.max(0, posY + dir * STEP));
sendCommand('Y' + posY);
}
}
function centerServos() {
posX = 90; posY = 90;
sendCommand('X90'); sendCommand('Y90');
}
// ── BOTONES COCHE ───────────────────────────────────────────────
document.querySelectorAll('#dpad-car .btn').forEach(btn => {
const cmd = btn.dataset.cmd;
const isMove = ['F','B','L','R'].includes(cmd);
const press = () => { btn.classList.add('pressed'); sendCommand(cmd); };
const release = () => {
btn.classList.remove('pressed');
if (isMove) sendCommand('S');
};
btn.addEventListener('mousedown', press);
btn.addEventListener('mouseup', release);
btn.addEventListener('mouseleave', release);
btn.addEventListener('touchstart', e => { e.preventDefault(); press(); }, { passive: false });
btn.addEventListener('touchend', e => { e.preventDefault(); release(); }, { passive: false });
btn.addEventListener('touchcancel',e => { e.preventDefault(); release(); }, { passive: false });
[['b-F','F'],['b-B','B'],['b-L','L'],['b-R','R']].forEach(([id,cmd])=>{
const el=document.getElementById(id);
const dn=()=>{el.classList.add('on');send(cmd);};
const up=()=>{el.classList.remove('on');send('S');};
el.addEventListener('mousedown',dn);el.addEventListener('mouseup',up);el.addEventListener('mouseleave',up);
el.addEventListener('touchstart',e=>{e.preventDefault();dn();},{passive:false});
el.addEventListener('touchend',e=>{e.preventDefault();up();},{passive:false});
el.addEventListener('touchcancel',e=>{e.preventDefault();up();},{passive:false});
});
// ── BOTONES CÁMARA ──────────────────────────────────────────────
document.querySelectorAll('#dpad-cam .btn').forEach(btn => {
const act = btn.dataset.cam;
const actions = {
YU: () => moveServo('Y', -1),
YD: () => moveServo('Y', 1),
XL: () => moveServo('X', 1),
XR: () => moveServo('X', -1),
C: () => centerServos(),
};
const press = () => { btn.classList.add('pressed'); actions[act]?.(); };
const release = () => btn.classList.remove('pressed');
btn.addEventListener('mousedown', press);
btn.addEventListener('mouseup', release);
btn.addEventListener('mouseleave', release);
btn.addEventListener('touchstart', e => { e.preventDefault(); press(); }, { passive: false });
btn.addEventListener('touchend', e => { e.preventDefault(); release(); }, { passive: false });
btn.addEventListener('touchcancel',e => { e.preventDefault(); release(); }, { passive: false });
const camA={'b-YU':()=>{pY=Math.max(0,pY-ST);send('Y'+pY);},'b-YD':()=>{pY=Math.min(180,pY+ST);send('Y'+pY);},'b-XL':()=>{pX=Math.min(180,pX+ST);send('X'+pX);},'b-XR':()=>{pX=Math.max(0,pX-ST);send('X'+pX);},'b-C':()=>{pX=90;pY=90;send('X90');send('Y90');}};
Object.entries(camA).forEach(([id,act])=>{
const el=document.getElementById(id);
const dn=()=>{el.classList.add('on');act();};const up=()=>el.classList.remove('on');
el.addEventListener('mousedown',dn);el.addEventListener('mouseup',up);el.addEventListener('mouseleave',up);
el.addEventListener('touchstart',e=>{e.preventDefault();dn();},{passive:false});
el.addEventListener('touchend',e=>{e.preventDefault();up();},{passive:false});
el.addEventListener('touchcancel',e=>{e.preventDefault();up();},{passive:false});
});
// ── BOCINA (hold to honk) ───────────────────────────────────────
const btnHorn = document.getElementById('btn-horn');
const startHorn = () => { if (hornOn) return; hornOn = true; btnHorn.classList.add('active'); sendCommand('H1'); };
const stopHorn = () => { if (!hornOn) return; hornOn = false; btnHorn.classList.remove('active'); sendCommand('H0'); };
btnHorn.addEventListener('mousedown', startHorn);
btnHorn.addEventListener('mouseup', stopHorn);
btnHorn.addEventListener('mouseleave', stopHorn);
btnHorn.addEventListener('touchstart', e => { e.preventDefault(); startHorn(); }, { passive: false });
btnHorn.addEventListener('touchend', e => { e.preventDefault(); stopHorn(); }, { passive: false });
btnHorn.addEventListener('touchcancel',e => { e.preventDefault(); stopHorn(); }, { passive: false });
const bH=document.getElementById('b-horn');
const sH=()=>{if(!hornOn){hornOn=true;bH.classList.add('on');send('H1');}};
const eH=()=>{if(hornOn){hornOn=false;bH.classList.remove('on');send('H0');}};
bH.addEventListener('mousedown',sH);bH.addEventListener('mouseup',eH);bH.addEventListener('mouseleave',eH);
bH.addEventListener('touchstart',e=>{e.preventDefault();sH();},{passive:false});
bH.addEventListener('touchend',e=>{e.preventDefault();eH();},{passive:false});
bH.addEventListener('touchcancel',e=>{e.preventDefault();eH();},{passive:false});
document.getElementById('b-led').addEventListener('click',()=>send('K'));
// ── LUZ ─────────────────────────────────────────────────────────
document.getElementById('btn-led').addEventListener('click', () => sendCommand('K'));
// ── TECLADO ─────────────────────────────────────────────────────
const held=new Set();
document.addEventListener('keydown',e=>{
if (e.repeat || held.has(e.key)) return;
held.add(e.key);
if(e.repeat||held.has(e.key))return;held.add(e.key);
switch(e.key){
case 'w': case 'W': sendCommand('F'); break;
case 's': case 'S': sendCommand('B'); break;
case 'a': case 'A': sendCommand('L'); break;
case 'd': case 'D': sendCommand('R'); break;
case 'ArrowUp': moveServo('Y', -1); break;
case 'ArrowDown': moveServo('Y', 1); break;
case 'ArrowLeft': moveServo('X', 1); break;
case 'ArrowRight': moveServo('X', -1); break;
case 'e': case 'E': startHorn(); break;
case 'f': case 'F': sendCommand('K'); break;
case 'r': case 'R': centerServos(); break;
case'w':case'W':send('F');break;case's':case'S':send('B');break;
case'a':case'A':send('L');break;case'd':case'D':send('R');break;
case'ArrowUp':pY=Math.max(0,pY-ST);send('Y'+pY);break;
case'ArrowDown':pY=Math.min(180,pY+ST);send('Y'+pY);break;
case'ArrowLeft':pX=Math.min(180,pX+ST);send('X'+pX);break;
case'ArrowRight':pX=Math.max(0,pX-ST);send('X'+pX);break;
case'e':case'E':sH();break;case'f':case'F':send('K');break;
case'r':case'R':pX=90;pY=90;send('X90');send('Y90');break;
case'+':case'=':spEl.value=Math.min(6,+spEl.value+1);applySpeed();break;
case'-':case'_':spEl.value=Math.max(1,+spEl.value-1);applySpeed();break;
}
});
document.addEventListener('keyup',e=>{
held.delete(e.key);
if (['w','W','s','S','a','A','d','D'].includes(e.key)) sendCommand('S');
if (['e','E'].includes(e.key)) stopHorn();
if('wWsSaAdD'.includes(e.key)&&e.key.trim())send('S');
if('eE'.includes(e.key))eH();
});
</script>
</body>

Binary file not shown.

Binary file not shown.

View File

@@ -1 +1 @@
2
3

19
gen_header.py Normal file
View File

@@ -0,0 +1,19 @@
import gzip, os, sys
os.chdir("d:/Proyectos/Arduino/Arduino-Car/Esp32Cam/data")
with open("index.html","rb") as f:
data = f.read()
with gzip.open("index.html.gz","wb",compresslevel=9) as f:
f.write(data)
compressed = open("index.html.gz","rb").read()
print(f"Original: {len(data)} bytes Gzipped: {len(compressed)} bytes")
lines = ["// Auto-generado desde data/index.html"]
lines += [f"#define index_html_gz_len {len(compressed)}"]
lines += ["static const uint8_t index_html_gz[] = {"]
for i in range(0, len(compressed), 16):
chunk = compressed[i:i+16]
lines.append(" " + ", ".join(f"0x{b:02X}" for b in chunk) + ",")
lines[-1] = lines[-1].rstrip(",")
lines += ["};"]
with open("../camera_index.h","w") as f:
f.write("\n".join(lines) + "\n")
print("camera_index.h generado OK")