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:
@@ -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"
|
||||
|
||||
@@ -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
@@ -3,459 +3,325 @@
|
||||
<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; }
|
||||
|
||||
/* 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 */
|
||||
#ctrl {
|
||||
position:fixed; bottom:0; left:0; right:0;
|
||||
padding:0 12px 14px;
|
||||
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;
|
||||
}
|
||||
|
||||
#stream {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── CONTROLS OVERLAY ───────────────────────────── */
|
||||
#controls {
|
||||
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 */
|
||||
}
|
||||
#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;
|
||||
width:100%; -webkit-appearance:none; appearance:none;
|
||||
height:4px; background:transparent; cursor:pointer; margin-top:5px; outline:none;
|
||||
}
|
||||
#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;
|
||||
#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; }
|
||||
|
||||
/* Extra buttons */
|
||||
.extras { display:flex; gap:8px; }
|
||||
.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); }
|
||||
|
||||
/* Extras */
|
||||
.extras { display: flex; gap: 8px; }
|
||||
.hint { font-size:7px; letter-spacing:2px; color:rgba(160,185,210,.18); text-align:center; line-height:2.2; }
|
||||
|
||||
.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;
|
||||
/* RESPONSIVE */
|
||||
@media (max-height:480px) {
|
||||
.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; }
|
||||
}
|
||||
.btn-extra:active { transform: scale(0.92); }
|
||||
.btn-extra.active {
|
||||
background: var(--orange);
|
||||
box-shadow: 0 0 12px rgba(244,162,97,.5);
|
||||
}
|
||||
|
||||
/* Keys hint */
|
||||
.hint { font-size: 9px; color: rgba(255,255,255,.2); text-align: center; line-height: 1.8; }
|
||||
|
||||
/* ── 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 ───────────────────── */
|
||||
@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; }
|
||||
@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 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 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">📯</button>
|
||||
<button class="btn-x" id="b-led">💡</button>
|
||||
</div>
|
||||
<div class="hint">WASD · ←→↑↓ · E · F · R</div>
|
||||
<div class="hint">W A S D · FLECHAS · E F 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=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();
|
||||
|
||||
function startStream() {
|
||||
view.src = streamSrc + '?t=' + Date.now();
|
||||
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(()=>{});
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
let pX=90,pY=90;const ST=10;
|
||||
|
||||
[['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});
|
||||
});
|
||||
|
||||
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});
|
||||
});
|
||||
|
||||
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'));
|
||||
|
||||
const held=new Set();
|
||||
document.addEventListener('keydown',e=>{
|
||||
if(e.repeat||held.has(e.key))return;held.add(e.key);
|
||||
switch(e.key){
|
||||
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;
|
||||
}
|
||||
view.onload = () => {
|
||||
dot.className = 'dot ok';
|
||||
barStatus.textContent = 'En vivo';
|
||||
};
|
||||
view.onerror = () => {
|
||||
dot.className = 'dot err';
|
||||
barStatus.textContent = 'Reconectando…';
|
||||
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}`;
|
||||
}
|
||||
|
||||
// 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(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── VELOCIDAD ───────────────────────────────────────────────────
|
||||
const speedEl = document.getElementById('speed');
|
||||
const barSpeed = document.getElementById('bar-speed');
|
||||
|
||||
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 });
|
||||
});
|
||||
|
||||
// ── 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 });
|
||||
});
|
||||
|
||||
// ── 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 });
|
||||
|
||||
// ── 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);
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', e => {
|
||||
});
|
||||
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>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
2
|
||||
3
|
||||
19
gen_header.py
Normal file
19
gen_header.py
Normal 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")
|
||||
Reference in New Issue
Block a user