147 lines
3.7 KiB
C++
147 lines
3.7 KiB
C++
#include <Arduino.h>
|
|
#include "web_ui.h"
|
|
|
|
const char INDEX_HTML[] PROGMEM = R"HTML(
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta charset="utf-8">
|
|
<title>ESP32 Robot</title>
|
|
<style>
|
|
body { font-family: sans-serif; margin: 16px; }
|
|
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; max-width: 420px; }
|
|
button { font-size: 18px; padding: 18px 10px; }
|
|
.wide { grid-column: span 3; }
|
|
.row2 { margin-top: 14px; max-width: 420px; }
|
|
input[type=range]{ width: 100%; }
|
|
.muted { opacity: 0.7; font-size: 13px; }
|
|
.status { margin-top: 10px; font-family: monospace; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>ESP32 Robot</h2>
|
|
|
|
<div class="grid">
|
|
<div></div>
|
|
<button id="btnF">▲</button>
|
|
<div></div>
|
|
|
|
<button id="btnL">◀</button>
|
|
<button id="btnS">■</button>
|
|
<button id="btnR">▶</button>
|
|
|
|
<div></div>
|
|
<button id="btnB">▼</button>
|
|
<div></div>
|
|
|
|
<button class="wide" id="btnStop">STOP</button>
|
|
</div>
|
|
|
|
<div class="row2">
|
|
<label>Speed: <span id="spv">150</span></label>
|
|
<input id="speed" type="range" min="0" max="255" value="150" />
|
|
<div class="muted">Tip: удерживай стрелку — робот едет. Отпустил — STOP.</div>
|
|
<div class="status" id="st"></div>
|
|
</div>
|
|
|
|
<div class="row2">
|
|
<button onclick="setMode('IDLE')">IDLE</button>
|
|
<button onclick="setMode('MANUAL')">MANUAL</button>
|
|
<button onclick="setMode('AUTO')">AUTO</button>
|
|
</div>
|
|
|
|
<script>
|
|
const st = (t) => document.getElementById('st').textContent = t;
|
|
let holdTimer = null;
|
|
|
|
async function cmd(c){
|
|
try{
|
|
const r = await fetch(`/cmd?c=${c}`);
|
|
st(await r.text());
|
|
}catch(e){
|
|
st('ERR');
|
|
}
|
|
}
|
|
|
|
async function setSpeed(v){
|
|
document.getElementById('spv').textContent = v;
|
|
try{
|
|
const r = await fetch(`/speed?l=${v}&r=${v}`);
|
|
st(await r.text());
|
|
}catch(e){
|
|
st('ERR');
|
|
}
|
|
}
|
|
|
|
async function setMode(m){
|
|
try{
|
|
const r = await fetch(`/mode?m=${m}`);
|
|
st(await r.text());
|
|
}catch(e){
|
|
st('ERR');
|
|
}
|
|
}
|
|
|
|
async function updateStatus(){
|
|
try{
|
|
const r = await fetch('/status');
|
|
const j = await r.json();
|
|
|
|
st(
|
|
`MODE=${j.mode} | ` +
|
|
`L=${j.speedL} R=${j.speedR} | ` +
|
|
`RSSI=${j.rssi}dBm | ` +
|
|
`UP=${Math.floor(j.uptime/1000)}s`
|
|
);
|
|
}catch(e){
|
|
st('STATUS ERR');
|
|
}
|
|
}
|
|
setInterval(updateStatus, 500);
|
|
|
|
// управление "пока держишь" (heartbeat)
|
|
function bindHold(btn, onPressCmd){
|
|
const press = (e)=>{
|
|
e.preventDefault();
|
|
cmd(onPressCmd);
|
|
holdTimer = setInterval(() => {
|
|
cmd(onPressCmd);
|
|
}, 200); // каждые 200 мс
|
|
};
|
|
|
|
const release = (e)=>{
|
|
e.preventDefault();
|
|
if (holdTimer) {
|
|
clearInterval(holdTimer);
|
|
holdTimer = null;
|
|
}
|
|
cmd('STOP');
|
|
};
|
|
|
|
btn.addEventListener('mousedown', press);
|
|
btn.addEventListener('touchstart', press, {passive:false});
|
|
|
|
btn.addEventListener('mouseup', release);
|
|
btn.addEventListener('mouseleave', release);
|
|
btn.addEventListener('touchend', release);
|
|
btn.addEventListener('touchcancel', release);
|
|
}
|
|
|
|
bindHold(document.getElementById('btnF'), 'FWD');
|
|
bindHold(document.getElementById('btnB'), 'BACK');
|
|
bindHold(document.getElementById('btnL'), 'LEFT');
|
|
bindHold(document.getElementById('btnR'), 'RIGHT');
|
|
|
|
document.getElementById('btnS').onclick = ()=>cmd('STOP');
|
|
document.getElementById('btnStop').onclick = ()=>cmd('STOP');
|
|
|
|
const slider = document.getElementById('speed');
|
|
slider.addEventListener('input', ()=> setSpeed(slider.value));
|
|
|
|
st('Ready');
|
|
</script>
|
|
</body>
|
|
</html>
|
|
)HTML";
|