Add Serial Terminal App & WebUi Theme
This commit is contained in:
@@ -3,6 +3,7 @@ idf_component_register(
|
||||
"web_server.c"
|
||||
"pages/wifi.c"
|
||||
"pages/ota.c"
|
||||
"pages/serial.c"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
REQUIRES
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
|
||||
void web_server_register_wifi_pages(httpd_handle_t server);
|
||||
void web_server_register_ota_page(httpd_handle_t server);
|
||||
void web_server_register_serial_pages(httpd_handle_t server);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,11 +14,124 @@ static void reboot_task(void *arg)
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
static const char OTA_HTML[] =
|
||||
"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\">"
|
||||
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">"
|
||||
"<title>ABrobot OTA Firmware Update</title>"
|
||||
"<style>"
|
||||
":root{--bg:#0b0f17;--card:#141b26;--border:#243042;--text:#e6edf3;--muted:#8b949e;--accent:#00d2ff;--accent2:#3a7bd5;--green:#00e676;--amber:#ffb300;--red:#ff5252;--term-bg:#080b10;}"
|
||||
"*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;}"
|
||||
"body{background:var(--bg);color:var(--text);padding:14px;display:flex;justify-content:center;min-height:100vh;}"
|
||||
".container{width:100%;max-width:920px;display:flex;flex-direction:column;gap:14px;}"
|
||||
".header{background:linear-gradient(135deg,rgba(58,123,213,0.18),rgba(0,210,255,0.08));border:1px solid var(--border);border-radius:12px;padding:12px 18px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;}"
|
||||
".title-group{display:flex;align-items:center;gap:10px;}"
|
||||
".status-dot{width:10px;height:10px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);}"
|
||||
".title{font-size:1.15rem;font-weight:700;letter-spacing:0.5px;background:linear-gradient(90deg,#00d2ff,#3a7bd5);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}"
|
||||
".nav-links{display:flex;gap:8px;align-items:center;}"
|
||||
".nav-links a{color:var(--muted);text-decoration:none;font-size:0.85rem;padding:6px 12px;border-radius:6px;transition:all 0.2s;}"
|
||||
".nav-links a:hover{color:var(--text);background:rgba(255,255,255,0.05);}"
|
||||
".nav-links a.active{color:var(--accent);font-weight:600;background:rgba(0,210,255,0.1);border:1px solid rgba(0,210,255,0.25);}"
|
||||
".card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:16px;box-shadow:0 4px 16px rgba(0,0,0,0.3);}"
|
||||
".card-title{font-size:1rem;font-weight:600;color:var(--text);display:flex;align-items:center;justify-content:space-between;}"
|
||||
".badge{background:#1a2332;border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:0.75rem;font-family:monospace;color:#58a6ff;}"
|
||||
".dropzone{background:#0a0e14;border:2px dashed var(--border);border-radius:10px;padding:32px 20px;text-align:center;cursor:pointer;transition:all 0.2s;display:flex;flex-direction:column;align-items:center;gap:10px;}"
|
||||
".dropzone:hover,.dropzone.dragover{border-color:var(--accent);background:rgba(0,210,255,0.04);box-shadow:0 0 12px rgba(0,210,255,0.15);}"
|
||||
".drop-icon{font-size:2.2rem;line-height:1;}"
|
||||
".drop-text{font-size:0.92rem;font-weight:600;color:var(--text);}"
|
||||
".drop-subtext{font-size:0.8rem;color:var(--muted);}"
|
||||
".file-info{display:none;background:#0a0e14;border:1px solid var(--border);border-radius:8px;padding:12px 16px;align-items:center;justify-content:space-between;}"
|
||||
".file-meta{display:flex;flex-direction:column;gap:3px;}"
|
||||
".file-name{font-size:0.9rem;font-weight:600;color:var(--accent);font-family:monospace;}"
|
||||
".file-size{font-size:0.78rem;color:var(--muted);}"
|
||||
"button{cursor:pointer;background:linear-gradient(180deg,#21262d,#161b22);border:1px solid #30363d;border-radius:8px;color:var(--text);padding:9px 16px;font-size:0.88rem;font-weight:600;transition:all 0.2s;display:inline-flex;align-items:center;justify-content:center;gap:6px;}"
|
||||
"button:hover{background:#30363d;border-color:var(--muted);}"
|
||||
"button.primary{background:linear-gradient(135deg,var(--accent2),var(--accent));border:none;color:#0a0d14;font-weight:700;padding:12px 20px;font-size:0.95rem;}"
|
||||
"button.primary:hover{opacity:0.92;box-shadow:0 0 14px rgba(0,210,255,0.4);}"
|
||||
"button:disabled{opacity:0.45;cursor:not-allowed;box-shadow:none;}"
|
||||
".progress-wrap{display:none;flex-direction:column;gap:8px;}"
|
||||
".progress-header{display:flex;justify-content:space-between;font-size:0.84rem;color:var(--muted);}"
|
||||
".progress-bar{background:var(--term-bg);border:1px solid var(--border);border-radius:8px;height:24px;overflow:hidden;position:relative;}"
|
||||
".progress-fill{background:linear-gradient(90deg,var(--accent2),var(--accent));height:100%;width:0%;transition:width 0.15s ease;box-shadow:0 0 10px rgba(0,210,255,0.4);}"
|
||||
".progress-pct{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:0.75rem;font-weight:700;font-family:monospace;color:#fff;text-shadow:0 1px 2px rgba(0,0,0,0.8);}"
|
||||
".msg-box{padding:12px 14px;border-radius:8px;font-size:0.86rem;line-height:1.45;display:none;}"
|
||||
".msg-info{background:rgba(58,123,213,0.12);border:1px solid rgba(58,123,213,0.35);color:#79c0ff;}"
|
||||
".msg-success{background:rgba(0,230,118,0.12);border:1px solid rgba(0,230,118,0.35);color:#7ee787;}"
|
||||
".msg-error{background:rgba(255,82,82,0.12);border:1px solid rgba(255,82,82,0.35);color:#ff7b72;}"
|
||||
".info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;}"
|
||||
".info-item{background:#0a0e14;border:1px solid var(--border);border-radius:8px;padding:12px;display:flex;flex-direction:column;gap:4px;}"
|
||||
".info-label{font-size:0.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;}"
|
||||
".info-val{font-size:0.85rem;color:var(--text);font-weight:500;}"
|
||||
"</style></head><body>"
|
||||
"<div class=\"container\">"
|
||||
"<div class=\"header\">"
|
||||
" <div class=\"title-group\">"
|
||||
" <div class=\"status-dot\" id=\"statusDot\"></div>"
|
||||
" <div class=\"title\">ESP32-C3 Firmware Update</div>"
|
||||
" </div>"
|
||||
" <div class=\"nav-links\">"
|
||||
" <a href=\"/\">Wi-Fi Setup</a>"
|
||||
" <a href=\"/serial\">Serial Terminal</a>"
|
||||
" <a href=\"/ota\" class=\"active\">OTA Update</a>"
|
||||
" </div>"
|
||||
"</div>"
|
||||
"<div class=\"card\">"
|
||||
" <div class=\"card-title\"><span>Over-The-Air Flasher</span><span class=\"badge\">OTA 2.0 Engine</span></div>"
|
||||
" <form id=\"otaForm\" onsubmit=\"startUpload(event)\" style=\"display:flex;flex-direction:column;gap:16px;\">"
|
||||
" <input type=\"file\" id=\"fileInput\" accept=\".bin,application/octet-stream\" style=\"display:none;\" onchange=\"fileChosen()\">"
|
||||
" <div id=\"dropArea\" class=\"dropzone\" onclick=\"document.getElementById('fileInput').click()\">"
|
||||
" <div class=\"drop-icon\">⚡</div>"
|
||||
" <div class=\"drop-text\">Click or Drag & Drop Firmware Image (.bin)</div>"
|
||||
" <div class=\"drop-subtext\">Select the compiled application binary file to flash to ESP32-C3</div>"
|
||||
" </div>"
|
||||
" <div id=\"fileCard\" class=\"file-info\">"
|
||||
" <div class=\"file-meta\">"
|
||||
" <span class=\"file-name\" id=\"fileNameText\">firmware.bin</span>"
|
||||
" <span class=\"file-size\" id=\"fileSizeText\">0 KB</span>"
|
||||
" </div>"
|
||||
" <button type=\"button\" onclick=\"document.getElementById('fileInput').click()\">Change</button>"
|
||||
" </div>"
|
||||
" <div id=\"progressSection\" class=\"progress-wrap\">"
|
||||
" <div class=\"progress-header\">"
|
||||
" <span id=\"progressStatus\">Uploading binary...</span>"
|
||||
" <span id=\"bytesTransferred\">0 KB / 0 KB</span>"
|
||||
" </div>"
|
||||
" <div class=\"progress-bar\">"
|
||||
" <div class=\"progress-fill\" id=\"progressFill\"></div>"
|
||||
" <div class=\"progress-pct\" id=\"progressPct\">0%</div>"
|
||||
" </div>"
|
||||
" </div>"
|
||||
" <button type=\"submit\" class=\"primary\" id=\"btnUpload\" disabled>Start Firmware Update</button>"
|
||||
" </form>"
|
||||
" <div id=\"statusMsg\" class=\"msg-box\"></div>"
|
||||
"</div>"
|
||||
"<div class=\"card\">"
|
||||
" <div class=\"card-title\"><span>OTA Safety Checklist</span></div>"
|
||||
" <div class=\"info-grid\">"
|
||||
" <div class=\"info-item\"><span class=\"info-label\">Dual Partition</span><span class=\"info-val\">Safe rollback on boot failure</span></div>"
|
||||
" <div class=\"info-item\"><span class=\"info-label\">Flash Verification</span><span class=\"info-val\">SHA-256 & image headers verified</span></div>"
|
||||
" <div class=\"info-item\"><span class=\"info-label\">Power Notice</span><span class=\"info-val\">Do not remove power during flash</span></div>"
|
||||
" <div class=\"info-item\"><span class=\"info-label\">Auto Restart</span><span class=\"info-val\">Reboots automatically after install</span></div>"
|
||||
" </div>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
"<script>"
|
||||
"const fileIn=document.getElementById('fileInput'),dropArea=document.getElementById('dropArea'),fileCard=document.getElementById('fileCard'),fileName=document.getElementById('fileNameText'),fileSize=document.getElementById('fileSizeText'),progSec=document.getElementById('progressSection'),progFill=document.getElementById('progressFill'),progPct=document.getElementById('progressPct'),progStatus=document.getElementById('progressStatus'),bytesText=document.getElementById('bytesTransferred'),btnUp=document.getElementById('btnUpload'),msg=document.getElementById('statusMsg'),statusDot=document.getElementById('statusDot');"
|
||||
"function showMsg(text,type){msg.className='msg-box msg-'+(type||'info');msg.textContent=text;msg.style.display='block';}"
|
||||
"function formatBytes(bytes){if(bytes<1024)return bytes+' B';if(bytes<1048576)return(bytes/1024).toFixed(1)+' KB';return(bytes/1048576).toFixed(2)+' MB';}"
|
||||
"function fileChosen(){if(fileIn.files.length>0){const f=fileIn.files[0];fileName.textContent=f.name;fileSize.textContent=formatBytes(f.size);dropArea.style.display='none';fileCard.style.display='flex';btnUp.disabled=false;showMsg('Ready to flash \"'+f.name+'\" ('+formatBytes(f.size)+'). Click \"Start Firmware Update\" to proceed.','info');}}"
|
||||
"['dragenter','dragover'].forEach(ev=>{dropArea.addEventListener(ev,e=>{e.preventDefault();dropArea.classList.add('dragover');});});"
|
||||
"['dragleave','drop'].forEach(ev=>{dropArea.addEventListener(ev,e=>{e.preventDefault();dropArea.classList.remove('dragover');});});"
|
||||
"dropArea.addEventListener('drop',e=>{if(e.dataTransfer.files.length>0){fileIn.files=e.dataTransfer.files;fileChosen();}});"
|
||||
"function startUpload(e){e.preventDefault();if(!fileIn.files.length)return;const file=fileIn.files[0];btnUp.disabled=true;btnUp.textContent='Flashing in Progress...';progSec.style.display='flex';progFill.style.width='0%';progPct.textContent='0%';showMsg('Uploading firmware payload... Please keep device powered on.','info');const xhr=new XMLHttpRequest();xhr.open('POST','/ota',true);xhr.setRequestHeader('Content-Type','application/octet-stream');xhr.upload.onprogress=ev=>{if(ev.lengthComputable){const pct=Math.round((ev.loaded/ev.total)*100);progFill.style.width=pct+'%';progPct.textContent=pct+'%';progStatus.textContent=pct<100?'Uploading firmware binary...':'Writing flash & verifying integrity...';bytesText.textContent=formatBytes(ev.loaded)+' / '+formatBytes(ev.total);}};"
|
||||
"xhr.onload=()=>{if(xhr.status===200){progFill.style.width='100%';progPct.textContent='100%';progStatus.textContent='OTA Flash Complete!';statusDot.style.background='#00e676';showMsg('Firmware updated successfully! Device is now rebooting... Please wait 5 seconds and refresh.','success');let countdown=5;setInterval(()=>{if(countdown>0){progStatus.textContent='Rebooting in '+countdown+'s...';countdown--;}else{window.location.href='/';}},1000);}else{statusDot.style.background='#ff5252';showMsg('OTA Flash Failed: '+(xhr.responseText||'Server Error '+xhr.status),'error');btnUp.disabled=false;btnUp.textContent='Retry Firmware Update';}};"
|
||||
"xhr.onerror=()=>{statusDot.style.background='#ff5252';showMsg('Upload network error occurred. Please verify Wi-Fi connection and retry.','error');btnUp.disabled=false;btnUp.textContent='Retry Firmware Update';};"
|
||||
"xhr.send(file);}"
|
||||
"</script></body></html>";
|
||||
|
||||
static esp_err_t ota_get(httpd_req_t *request)
|
||||
{
|
||||
const char *page = "<!doctype html><html><head><meta name=viewport content=\"width=device-width\"><title>ABrobot OTA</title><style>body{font:16px sans-serif;max-width:480px;margin:2em auto;padding:0 1em}input,button{font:inherit;padding:.6em;width:100%;box-sizing:border-box}button{margin-top:1em}progress{width:100%;margin-top:1em}</style></head><body><h1>Firmware update</h1><form id=form><input id=file type=file accept=\".bin,application/octet-stream\" required><button type=submit>Upload firmware</button></form><progress id=progress value=0 max=100 hidden></progress><p id=message></p><script>form.onsubmit=async e=>{e.preventDefault();if(!file.files.length)return;progress.hidden=false;message.textContent='Uploading...';try{let r=await fetch('/ota',{method:'POST',headers:{'Content-Type':'application/octet-stream'},body:file.files[0]});if(!r.ok)throw Error(await r.text());progress.value=100;message.textContent='Update installed. Rebooting...'}catch(e){message.textContent='Update failed: '+e.message;progress.hidden=true}}</script></body></html>";
|
||||
httpd_resp_set_type(request, "text/html");
|
||||
return httpd_resp_send(request, page, HTTPD_RESP_USE_STRLEN);
|
||||
return httpd_resp_send(request, OTA_HTML, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
static esp_err_t ota_post(httpd_req_t *request)
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
#include "web_server_pages.h"
|
||||
#include "sys_uart.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_http_server.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "SYS_WEB_SERIAL";
|
||||
|
||||
static const char SERIAL_HTML[] =
|
||||
"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\">"
|
||||
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">"
|
||||
"<title>ABrobot Web Serial Terminal</title>"
|
||||
"<style>"
|
||||
":root{--bg:#0b0f17;--card:#141b26;--border:#243042;--text:#e6edf3;--accent:#00d2ff;--accent2:#3a7bd5;--green:#00e676;--amber:#ffb300;--term-bg:#080b10;}"
|
||||
"*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;}"
|
||||
"body{background:var(--bg);color:var(--text);padding:14px;display:flex;justify-content:center;min-height:100vh;}"
|
||||
".container{width:100%;max-width:920px;display:flex;flex-direction:column;gap:12px;}"
|
||||
".header{background:linear-gradient(135deg,rgba(58,123,213,0.18),rgba(0,210,255,0.08));border:1px solid var(--border);border-radius:12px;padding:12px 18px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;}"
|
||||
".title-group{display:flex;align-items:center;gap:10px;}"
|
||||
".status-dot{width:10px;height:10px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);transition:background 0.3s;}"
|
||||
".title{font-size:1.15rem;font-weight:700;letter-spacing:0.5px;background:linear-gradient(90deg,#00d2ff,#3a7bd5);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}"
|
||||
".nav-links{display:flex;gap:8px;align-items:center;}"
|
||||
".nav-links a{color:#8b949e;text-decoration:none;font-size:0.85rem;padding:6px 12px;border-radius:6px;transition:all 0.2s;}"
|
||||
".nav-links a:hover{color:var(--text);background:rgba(255,255,255,0.05);}"
|
||||
".nav-links a.active{color:var(--accent);font-weight:600;background:rgba(0,210,255,0.1);border:1px solid rgba(0,210,255,0.25);}"
|
||||
".toolbar{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;}"
|
||||
".tool-group{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}"
|
||||
"label{font-size:0.82rem;color:#8b949e;}"
|
||||
"select,button,input{background:#0a0e14;color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:0.86rem;outline:none;transition:border-color 0.2s;}"
|
||||
"select:focus,input:focus{border-color:var(--accent);}"
|
||||
"button{cursor:pointer;background:linear-gradient(180deg,#21262d,#161b22);border-color:#30363d;font-weight:500;}"
|
||||
"button:hover{background:#30363d;border-color:#8b949e;}"
|
||||
"button.primary{background:linear-gradient(135deg,var(--accent2),var(--accent));border:none;color:#0a0d14;font-weight:700;}"
|
||||
"button.primary:hover{opacity:0.9;}"
|
||||
".badge{background:#1a2332;border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:0.75rem;font-family:monospace;color:#58a6ff;}"
|
||||
"#terminal{background:var(--term-bg);border:1px solid var(--border);border-radius:10px;height:420px;overflow-y:auto;padding:12px;font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace;font-size:0.86rem;line-height:1.45;white-space:pre-wrap;word-break:break-all;box-shadow:inset 0 2px 8px rgba(0,0,0,0.6);}"
|
||||
".log-rx{color:#7ee787;}"
|
||||
".log-tx{color:#f6d365;}"
|
||||
".log-sys{color:#79c0ff;font-style:italic;}"
|
||||
".log-hex{color:#d2a8ff;}"
|
||||
".input-bar{display:flex;gap:8px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px;flex-wrap:wrap;}"
|
||||
"#cmdInput{flex:1;min-width:200px;font-family:monospace;}"
|
||||
".macros{display:flex;gap:6px;flex-wrap:wrap;align-items:center;}"
|
||||
".macros button{font-size:0.75rem;padding:4px 8px;}"
|
||||
"@media(max-width:600px){#terminal{height:300px;}}"
|
||||
"</style></head><body>"
|
||||
"<div class=\"container\">"
|
||||
"<div class=\"header\">"
|
||||
" <div class=\"title-group\">"
|
||||
" <div class=\"status-dot\" id=\"statusDot\"></div>"
|
||||
" <div class=\"title\">ESP32-C3 Web Serial Terminal</div>"
|
||||
" </div>"
|
||||
" <div class=\"nav-links\">"
|
||||
" <a href=\"/\">Wi-Fi Setup</a>"
|
||||
" <a href=\"/serial\" class=\"active\">Serial Terminal</a>"
|
||||
" <a href=\"/ota\">OTA Update</a>"
|
||||
" </div>"
|
||||
"</div>"
|
||||
"<div class=\"toolbar\">"
|
||||
" <div class=\"tool-group\">"
|
||||
" <label for=\"baudSelect\">Baud:</label>"
|
||||
" <select id=\"baudSelect\" onchange=\"changeBaud()\">"
|
||||
" <option value=\"9600\">9600</option>"
|
||||
" <option value=\"19200\">19200</option>"
|
||||
" <option value=\"38400\">38400</option>"
|
||||
" <option value=\"57600\">57600</option>"
|
||||
" <option value=\"115200\" selected>115200</option>"
|
||||
" <option value=\"230400\">230400</option>"
|
||||
" <option value=\"460800\">460800</option>"
|
||||
" <option value=\"921600\">921600</option>"
|
||||
" </select>"
|
||||
" <label for=\"viewMode\">View:</label>"
|
||||
" <select id=\"viewMode\" onchange=\"clearTerminal()\">"
|
||||
" <option value=\"ascii\" selected>ASCII Text</option>"
|
||||
" <option value=\"hex\">HEX Dump</option>"
|
||||
" </select>"
|
||||
" <button onclick=\"clearTerminal()\">Clear</button>"
|
||||
" <button id=\"scrollBtn\" onclick=\"toggleAutoScroll()\">Scroll: ON</button>"
|
||||
" </div>"
|
||||
" <div class=\"tool-group\">"
|
||||
" <span class=\"badge\" id=\"rxStat\">RX: 0 B</span>"
|
||||
" <span class=\"badge\" id=\"txStat\">TX: 0 B</span>"
|
||||
" <button onclick=\"exportLog()\">Save Log</button>"
|
||||
" </div>"
|
||||
"</div>"
|
||||
"<div id=\"terminal\"></div>"
|
||||
"<div class=\"input-bar\">"
|
||||
" <select id=\"sendFormat\">"
|
||||
" <option value=\"text\" selected>Text</option>"
|
||||
" <option value=\"hex\">HEX (e.g. 41 42 FF)</option>"
|
||||
" </select>"
|
||||
" <select id=\"lineEnding\">"
|
||||
" <option value=\"\\r\\n\" selected>CRLF (\\r\\n)</option>"
|
||||
" <option value=\"\\n\">LF (\\n)</option>"
|
||||
" <option value=\"\\r\">CR (\\r)</option>"
|
||||
" <option value=\"\">None</option>"
|
||||
" </select>"
|
||||
" <input type=\"text\" id=\"cmdInput\" placeholder=\"Type command to send over TX (GPIO 21)...\" autocomplete=\"off\" autofocus>"
|
||||
" <button class=\"primary\" onclick=\"sendCmd()\">Send</button>"
|
||||
"</div>"
|
||||
"<div class=\"macros\">"
|
||||
" <span style=\"font-size:0.75rem;color:#8b949e;\">Quick:</span>"
|
||||
" <button onclick=\"sendMacro('PING')\">PING</button>"
|
||||
" <button onclick=\"sendMacro('AT')\">AT</button>"
|
||||
" <button onclick=\"sendMacro('HELP')\">HELP</button>"
|
||||
" <button onclick=\"sendMacro('STATUS')\">STATUS</button>"
|
||||
" <button onclick=\"sendMacro('RESET')\">RESET</button>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
"<script>"
|
||||
"let seq = 0, autoScroll = true, history = [], histIdx = -1;"
|
||||
"const term = document.getElementById('terminal');"
|
||||
"const cmdIn = document.getElementById('cmdInput');"
|
||||
"const rxStat = document.getElementById('rxStat');"
|
||||
"const txStat = document.getElementById('txStat');"
|
||||
"const baudSel = document.getElementById('baudSelect');"
|
||||
"const viewMode = document.getElementById('viewMode');"
|
||||
"const sendFmt = document.getElementById('sendFormat');"
|
||||
"const statusDot = document.getElementById('statusDot');"
|
||||
"function log(text, cls) {"
|
||||
" const span = document.createElement('span');"
|
||||
" span.className = cls || '';"
|
||||
" span.textContent = text;"
|
||||
" term.appendChild(span);"
|
||||
" if (autoScroll) term.scrollTop = term.scrollHeight;"
|
||||
"}"
|
||||
"function toggleAutoScroll() {"
|
||||
" autoScroll = !autoScroll;"
|
||||
" document.getElementById('scrollBtn').textContent = 'Scroll: ' + (autoScroll ? 'ON' : 'OFF');"
|
||||
"}"
|
||||
"async function clearTerminal() {"
|
||||
" term.innerHTML = '';"
|
||||
" try { await fetch('/serial/clear', {method: 'POST'}); } catch(e){}"
|
||||
" log('[Terminal buffer cleared]\\n', 'log-sys');"
|
||||
"}"
|
||||
"function exportLog() {"
|
||||
" const blob = new Blob([term.textContent], {type: 'text/plain'});"
|
||||
" const a = document.createElement('a');"
|
||||
" a.href = URL.createObjectURL(blob);"
|
||||
" a.download = 'serial_log_' + new Date().toISOString().replace(/[:.]/g,'-') + '.txt';"
|
||||
" a.click();"
|
||||
"}"
|
||||
"async function pollData() {"
|
||||
" try {"
|
||||
" const res = await fetch('/serial/data?seq=' + seq);"
|
||||
" if (res.ok) {"
|
||||
" statusDot.style.background = '#00e676';"
|
||||
" statusDot.style.boxShadow = '0 0 8px #00e676';"
|
||||
" const data = await res.json();"
|
||||
" if (data.seq !== undefined) seq = data.seq;"
|
||||
" if (data.rx !== undefined) rxStat.textContent = 'RX: ' + formatBytes(data.rx);"
|
||||
" if (data.tx !== undefined) txStat.textContent = 'TX: ' + formatBytes(data.tx);"
|
||||
" if (data.baud !== undefined && baudSel.value != data.baud) baudSel.value = data.baud;"
|
||||
" if (data.hex && data.hex.length > 0) {"
|
||||
" if (viewMode.value === 'hex') {"
|
||||
" let hexStr = '';"
|
||||
" for (let i = 0; i < data.hex.length; i++) {"
|
||||
" hexStr += data.hex[i].toString(16).padStart(2, '0').toUpperCase() + ' ';"
|
||||
" if ((i + 1) % 16 === 0) hexStr += '\\n';"
|
||||
" }"
|
||||
" log(hexStr, 'log-hex');"
|
||||
" } else {"
|
||||
" if (data.text) log(data.text, 'log-rx');"
|
||||
" }"
|
||||
" }"
|
||||
" } else {"
|
||||
" statusDot.style.background = '#ff5252';"
|
||||
" statusDot.style.boxShadow = '0 0 8px #ff5252';"
|
||||
" }"
|
||||
" } catch (e) {"
|
||||
" statusDot.style.background = '#ff5252';"
|
||||
" statusDot.style.boxShadow = '0 0 8px #ff5252';"
|
||||
" }"
|
||||
" setTimeout(pollData, 120);"
|
||||
"}"
|
||||
"function formatBytes(bytes) {"
|
||||
" if (bytes < 1024) return bytes + ' B';"
|
||||
" if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';"
|
||||
" return (bytes / 1048576).toFixed(2) + ' MB';"
|
||||
"}"
|
||||
"async function sendCmd() {"
|
||||
" let val = cmdIn.value;"
|
||||
" if (val === '') return;"
|
||||
" history.push(val);"
|
||||
" histIdx = history.length;"
|
||||
" cmdIn.value = '';"
|
||||
" let payload = '';"
|
||||
" if (sendFmt.value === 'hex') {"
|
||||
" let bytes = val.trim().split(/[\\s,]+/).map(h => parseInt(h, 16)).filter(n => !isNaN(n));"
|
||||
" let binStr = String.fromCharCode(...bytes);"
|
||||
" payload = binStr;"
|
||||
" log('-> [HEX: ' + val + ']\\n', 'log-tx');"
|
||||
" } else {"
|
||||
" const le = document.getElementById('lineEnding').value;"
|
||||
" payload = val + le.replace(/\\\\r/g, '\\r').replace(/\\\\n/g, '\\n');"
|
||||
" log('-> ' + val + '\\n', 'log-tx');"
|
||||
" }"
|
||||
" try {"
|
||||
" await fetch('/serial/tx', {method: 'POST', body: payload});"
|
||||
" } catch (e) {"
|
||||
" log('[TX Error: ' + e.message + ']\\n', 'log-sys');"
|
||||
" }"
|
||||
"}"
|
||||
"function sendMacro(cmd) {"
|
||||
" cmdIn.value = cmd;"
|
||||
" sendCmd();"
|
||||
"}"
|
||||
"async function changeBaud() {"
|
||||
" const b = baudSel.value;"
|
||||
" log('[Switching Baud to ' + b + '...]\\n', 'log-sys');"
|
||||
" try {"
|
||||
" await fetch('/serial/baud', {method: 'POST', body: b});"
|
||||
" await clearTerminal();"
|
||||
" } catch (e) {"
|
||||
" log('[Failed to set baud: ' + e.message + ']\\n', 'log-sys');"
|
||||
" }"
|
||||
"}"
|
||||
"cmdIn.addEventListener('keydown', e => {"
|
||||
" if (e.key === 'Enter') { sendCmd(); }"
|
||||
" else if (e.key === 'ArrowUp') {"
|
||||
" if (histIdx > 0) { histIdx--; cmdIn.value = history[histIdx]; }"
|
||||
" } else if (e.key === 'ArrowDown') {"
|
||||
" if (histIdx < history.length - 1) { histIdx++; cmdIn.value = history[histIdx]; }"
|
||||
" else { histIdx = history.length; cmdIn.value = ''; }"
|
||||
" }"
|
||||
"});"
|
||||
"log('[ABrobot Web Serial Connected on TX:GPIO21 / RX:GPIO20 (Internal Pull-Up Active)]\\n', 'log-sys');"
|
||||
"pollData();"
|
||||
"</script></body></html>";
|
||||
|
||||
static esp_err_t serial_page_get(httpd_req_t *request)
|
||||
{
|
||||
httpd_resp_set_type(request, "text/html");
|
||||
return httpd_resp_send(request, SERIAL_HTML, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
static esp_err_t serial_data_get(httpd_req_t *request)
|
||||
{
|
||||
char query[64] = {0};
|
||||
uint32_t from_seq = 0;
|
||||
if (httpd_req_get_url_query_str(request, query, sizeof(query)) == ESP_OK) {
|
||||
char val[16] = {0};
|
||||
if (httpd_query_key_value(query, "seq", val, sizeof(val)) == ESP_OK) {
|
||||
from_seq = (uint32_t)strtoul(val, NULL, 10);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t rx_delta[1024];
|
||||
uint32_t new_seq = from_seq;
|
||||
size_t len = sys_uart_read_delta(from_seq, rx_delta, sizeof(rx_delta) - 1, &new_seq);
|
||||
rx_delta[len] = '\0';
|
||||
|
||||
uint32_t total_rx = 0, total_tx = 0;
|
||||
sys_uart_get_stats(&total_rx, &total_tx);
|
||||
uint32_t baud = sys_uart_get_baud_rate();
|
||||
|
||||
httpd_resp_set_type(request, "application/json");
|
||||
|
||||
/* Begin JSON response */
|
||||
char prefix[128];
|
||||
int p_len = snprintf(prefix, sizeof(prefix), "{\"seq\":%lu,\"rx\":%lu,\"tx\":%lu,\"baud\":%lu,\"text\":\"",
|
||||
(unsigned long)new_seq, (unsigned long)total_rx, (unsigned long)total_tx, (unsigned long)baud);
|
||||
httpd_resp_send_chunk(request, prefix, p_len);
|
||||
|
||||
/* Stream escaped JSON text */
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
uint8_t c = rx_delta[i];
|
||||
if (c == '"') {
|
||||
httpd_resp_send_chunk(request, "\\\"", 2);
|
||||
} else if (c == '\\') {
|
||||
httpd_resp_send_chunk(request, "\\\\", 2);
|
||||
} else if (c == '\n') {
|
||||
httpd_resp_send_chunk(request, "\\n", 2);
|
||||
} else if (c == '\r') {
|
||||
httpd_resp_send_chunk(request, "\\r", 2);
|
||||
} else if (c == '\t') {
|
||||
httpd_resp_send_chunk(request, "\\t", 2);
|
||||
} else if (c < 32 || c > 126) {
|
||||
/* Replace non-printable ASCII in text view with dot or hex escape */
|
||||
char hex_esc[8];
|
||||
int h_len = snprintf(hex_esc, sizeof(hex_esc), "\\u%04x", c);
|
||||
httpd_resp_send_chunk(request, hex_esc, h_len);
|
||||
} else {
|
||||
char ch[2] = {(char)c, '\0'};
|
||||
httpd_resp_send_chunk(request, ch, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Stream raw byte array for HEX dump view */
|
||||
httpd_resp_send_chunk(request, "\",\"hex\":[", 9);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char num[8];
|
||||
int n_len = snprintf(num, sizeof(num), "%s%u", (i > 0 ? "," : ""), (unsigned int)rx_delta[i]);
|
||||
httpd_resp_send_chunk(request, num, n_len);
|
||||
}
|
||||
httpd_resp_send_chunk(request, "]}", 2);
|
||||
|
||||
return httpd_resp_send_chunk(request, NULL, 0);
|
||||
}
|
||||
|
||||
static esp_err_t serial_tx_post(httpd_req_t *request)
|
||||
{
|
||||
char body[512] = {0};
|
||||
int total_len = request->content_len;
|
||||
if (total_len >= sizeof(body)) {
|
||||
total_len = sizeof(body) - 1;
|
||||
}
|
||||
|
||||
int received = httpd_req_recv(request, body, total_len);
|
||||
if (received > 0) {
|
||||
sys_uart_write((const uint8_t *)body, (size_t)received);
|
||||
httpd_resp_sendstr(request, "OK");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "No TX data received");
|
||||
}
|
||||
|
||||
static esp_err_t serial_baud_post(httpd_req_t *request)
|
||||
{
|
||||
char body[32] = {0};
|
||||
int received = httpd_req_recv(request, body, sizeof(body) - 1);
|
||||
if (received > 0) {
|
||||
body[received] = '\0';
|
||||
uint32_t baud = (uint32_t)strtoul(body, NULL, 10);
|
||||
if (baud > 0) {
|
||||
esp_err_t err = sys_uart_set_baud_rate(baud);
|
||||
if (err == ESP_OK) {
|
||||
httpd_resp_sendstr(request, "OK");
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Invalid baud rate");
|
||||
}
|
||||
|
||||
static esp_err_t serial_clear_post(httpd_req_t *request)
|
||||
{
|
||||
sys_uart_clear_rx_buffer();
|
||||
httpd_resp_sendstr(request, "OK");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void web_server_register_serial_pages(httpd_handle_t server)
|
||||
{
|
||||
const httpd_uri_t page = {.uri = "/serial", .method = HTTP_GET, .handler = serial_page_get};
|
||||
const httpd_uri_t data = {.uri = "/serial/data", .method = HTTP_GET, .handler = serial_data_get};
|
||||
const httpd_uri_t tx = {.uri = "/serial/tx", .method = HTTP_POST, .handler = serial_tx_post};
|
||||
const httpd_uri_t baud = {.uri = "/serial/baud", .method = HTTP_POST, .handler = serial_baud_post};
|
||||
const httpd_uri_t clear = {.uri = "/serial/clear", .method = HTTP_POST, .handler = serial_clear_post};
|
||||
|
||||
httpd_register_uri_handler(server, &page);
|
||||
httpd_register_uri_handler(server, &data);
|
||||
httpd_register_uri_handler(server, &tx);
|
||||
httpd_register_uri_handler(server, &baud);
|
||||
httpd_register_uri_handler(server, &clear);
|
||||
ESP_LOGI(TAG, "Web Serial Terminal endpoints registered at /serial");
|
||||
}
|
||||
@@ -11,6 +11,115 @@ static const char *TAG = "SYS_WEB_WIFI";
|
||||
static char s_ssid[33];
|
||||
static char s_password[65];
|
||||
|
||||
static const char WIFI_HTML[] =
|
||||
"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\">"
|
||||
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">"
|
||||
"<title>ABrobot Wi-Fi Manager</title>"
|
||||
"<style>"
|
||||
":root{--bg:#0b0f17;--card:#141b26;--border:#243042;--text:#e6edf3;--muted:#8b949e;--accent:#00d2ff;--accent2:#3a7bd5;--green:#00e676;--amber:#ffb300;--red:#ff5252;}"
|
||||
"*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;}"
|
||||
"body{background:var(--bg);color:var(--text);padding:14px;display:flex;justify-content:center;min-height:100vh;}"
|
||||
".container{width:100%;max-width:920px;display:flex;flex-direction:column;gap:14px;}"
|
||||
".header{background:linear-gradient(135deg,rgba(58,123,213,0.18),rgba(0,210,255,0.08));border:1px solid var(--border);border-radius:12px;padding:12px 18px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;}"
|
||||
".title-group{display:flex;align-items:center;gap:10px;}"
|
||||
".status-dot{width:10px;height:10px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);}"
|
||||
".title{font-size:1.15rem;font-weight:700;letter-spacing:0.5px;background:linear-gradient(90deg,#00d2ff,#3a7bd5);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}"
|
||||
".nav-links{display:flex;gap:8px;align-items:center;}"
|
||||
".nav-links a{color:var(--muted);text-decoration:none;font-size:0.85rem;padding:6px 12px;border-radius:6px;transition:all 0.2s;}"
|
||||
".nav-links a:hover{color:var(--text);background:rgba(255,255,255,0.05);}"
|
||||
".nav-links a.active{color:var(--accent);font-weight:600;background:rgba(0,210,255,0.1);border:1px solid rgba(0,210,255,0.25);}"
|
||||
".card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:16px;box-shadow:0 4px 16px rgba(0,0,0,0.3);}"
|
||||
".card-title{font-size:1rem;font-weight:600;color:var(--text);display:flex;align-items:center;gap:8px;}"
|
||||
".form-group{display:flex;flex-direction:column;gap:6px;}"
|
||||
"label{font-size:0.84rem;color:var(--muted);font-weight:500;display:flex;justify-content:space-between;align-items:center;}"
|
||||
"select,input[type='text'],input[type='password']{background:#0a0e14;color:var(--text);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:0.9rem;outline:none;width:100%;transition:border-color 0.2s,box-shadow 0.2s;}"
|
||||
"select:focus,input[type='text']:focus,input[type='password']:focus{border-color:var(--accent);box-shadow:0 0 8px rgba(0,210,255,0.25);}"
|
||||
"button{cursor:pointer;background:linear-gradient(180deg,#21262d,#161b22);border:1px solid #30363d;border-radius:8px;color:var(--text);padding:9px 16px;font-size:0.88rem;font-weight:600;transition:all 0.2s;display:inline-flex;align-items:center;justify-content:center;gap:6px;}"
|
||||
"button:hover{background:#30363d;border-color:var(--muted);}"
|
||||
"button.primary{background:linear-gradient(135deg,var(--accent2),var(--accent));border:none;color:#0a0d14;font-weight:700;padding:11px 20px;font-size:0.92rem;}"
|
||||
"button.primary:hover{opacity:0.92;box-shadow:0 0 12px rgba(0,210,255,0.35);}"
|
||||
"button:disabled{opacity:0.5;cursor:not-allowed;}"
|
||||
".input-row{display:flex;gap:10px;align-items:center;}"
|
||||
".pass-wrap{position:relative;display:flex;width:100%;}"
|
||||
".pass-wrap input{padding-right:42px;}"
|
||||
".pass-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;padding:4px 6px;color:var(--muted);font-size:0.9rem;cursor:pointer;border-radius:4px;}"
|
||||
".pass-toggle:hover{color:var(--text);background:rgba(255,255,255,0.1);}"
|
||||
".checkbox-wrap{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:0.85rem;color:var(--muted);user-select:none;}"
|
||||
".checkbox-wrap input{cursor:pointer;accent-color:var(--accent);width:16px;height:16px;}"
|
||||
".msg-box{padding:12px 14px;border-radius:8px;font-size:0.86rem;line-height:1.45;display:none;}"
|
||||
".msg-info{background:rgba(58,123,213,0.12);border:1px solid rgba(58,123,213,0.35);color:#79c0ff;}"
|
||||
".msg-success{background:rgba(0,230,118,0.12);border:1px solid rgba(0,230,118,0.35);color:#7ee787;}"
|
||||
".msg-error{background:rgba(255,82,82,0.12);border:1px solid rgba(255,82,82,0.35);color:#ff7b72;}"
|
||||
".badges{display:flex;gap:8px;flex-wrap:wrap;}"
|
||||
".badge{background:#1a2332;border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:0.75rem;font-family:monospace;color:#58a6ff;}"
|
||||
".info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;}"
|
||||
".info-item{background:#0a0e14;border:1px solid var(--border);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:4px;}"
|
||||
".info-label{font-size:0.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;}"
|
||||
".info-val{font-size:0.88rem;color:var(--text);font-family:monospace;font-weight:600;}"
|
||||
"</style></head><body>"
|
||||
"<div class=\"container\">"
|
||||
"<div class=\"header\">"
|
||||
" <div class=\"title-group\">"
|
||||
" <div class=\"status-dot\" id=\"statusDot\"></div>"
|
||||
" <div class=\"title\">ESP32-C3 Wi-Fi Setup</div>"
|
||||
" </div>"
|
||||
" <div class=\"nav-links\">"
|
||||
" <a href=\"/\" class=\"active\">Wi-Fi Setup</a>"
|
||||
" <a href=\"/serial\">Serial Terminal</a>"
|
||||
" <a href=\"/ota\">OTA Update</a>"
|
||||
" </div>"
|
||||
"</div>"
|
||||
"<div class=\"card\">"
|
||||
" <div class=\"card-title\"><span>Wireless Network Configuration</span></div>"
|
||||
" <form id=\"wifiForm\" onsubmit=\"submitWifi(event)\" style=\"display:flex;flex-direction:column;gap:14px;\">"
|
||||
" <div class=\"form-group\">"
|
||||
" <label for=\"networks\"><span>Available Wi-Fi Networks</span><span id=\"scanCount\" class=\"badge\">Ready</span></label>"
|
||||
" <div class=\"input-row\">"
|
||||
" <select id=\"networks\"><option value=\"\">Scanning nearby networks...</option></select>"
|
||||
" <button type=\"button\" id=\"btnScan\" onclick=\"scan()\">Refresh</button>"
|
||||
" </div>"
|
||||
" </div>"
|
||||
" <label class=\"checkbox-wrap\">"
|
||||
" <input type=\"checkbox\" id=\"hidden\" onchange=\"toggleManual()\">"
|
||||
" <span>Manual / Hidden Network SSID</span>"
|
||||
" </label>"
|
||||
" <div class=\"form-group\" id=\"manualBox\" style=\"display:none;\">"
|
||||
" <label for=\"manual\">Custom SSID</label>"
|
||||
" <input type=\"text\" id=\"manual\" name=\"ssid\" maxlength=\"32\" placeholder=\"Enter Wi-Fi network name...\">"
|
||||
" </div>"
|
||||
" <div class=\"form-group\">"
|
||||
" <label for=\"password\">Wi-Fi Password</label>"
|
||||
" <div class=\"pass-wrap\">"
|
||||
" <input type=\"password\" id=\"password\" name=\"password\" maxlength=\"64\" placeholder=\"Enter Wi-Fi password (WPA/WPA2)...\" required>"
|
||||
" <button type=\"button\" class=\"pass-toggle\" id=\"eyeBtn\" onclick=\"togglePassword()\" title=\"Show/Hide Password\">👁️</button>"
|
||||
" </div>"
|
||||
" </div>"
|
||||
" <button type=\"submit\" class=\"primary\" id=\"btnConnect\">Connect to Network</button>"
|
||||
" </form>"
|
||||
" <div id=\"statusMsg\" class=\"msg-box\"></div>"
|
||||
"</div>"
|
||||
"<div class=\"card\">"
|
||||
" <div class=\"card-title\"><span>Device Information</span></div>"
|
||||
" <div class=\"info-grid\">"
|
||||
" <div class=\"info-item\"><span class=\"info-label\">Provisioning AP</span><span class=\"info-val\">ABrobot-OS</span></div>"
|
||||
" <div class=\"info-item\"><span class=\"info-label\">Default AP IP</span><span class=\"info-val\">192.168.4.1</span></div>"
|
||||
" <div class=\"info-item\"><span class=\"info-label\">Hardware Port</span><span class=\"info-val\">TX:GPIO21 / RX:GPIO20</span></div>"
|
||||
" <div class=\"info-item\"><span class=\"info-label\">Status</span><span class=\"info-val\" style=\"color:var(--green);\">Operational</span></div>"
|
||||
" </div>"
|
||||
"</div>"
|
||||
"</div>"
|
||||
"<script>"
|
||||
"const list=document.getElementById('networks'),manual=document.getElementById('manual'),manualBox=document.getElementById('manualBox'),hidden=document.getElementById('hidden'),passInput=document.getElementById('password'),msg=document.getElementById('statusMsg'),btnConnect=document.getElementById('btnConnect'),btnScan=document.getElementById('btnScan'),scanCount=document.getElementById('scanCount');"
|
||||
"function showMsg(text,type){msg.className='msg-box msg-'+(type||'info');msg.textContent=text;msg.style.display='block';}"
|
||||
"function toggleManual(){let on=hidden.checked;manualBox.style.display=on?'block':'none';manual.required=on;if(on){manual.value='';manual.focus();}else if(list.value)manual.value=list.value;}"
|
||||
"function togglePassword(){let isPass=passInput.type==='password';passInput.type=isPass?'text':'password';document.getElementById('eyeBtn').textContent=isPass?'🔒':'👁️';}"
|
||||
"async function scan(){btnScan.disabled=true;btnScan.textContent='Scanning...';scanCount.textContent='Scanning...';showMsg('Scanning for 2.4 GHz Wi-Fi access points...','info');try{const res=await fetch('/scan');if(!res.ok)throw new Error('HTTP '+res.status);const data=await res.json();list.innerHTML='';if(!data.length){list.innerHTML='<option value=\"\">No networks found</option>';scanCount.textContent='0 APs';showMsg('No networks found. Check router or use manual SSID entry.','error');}else{data.forEach(n=>{const opt=document.createElement('option');opt.value=n.ssid;const qual=n.rssi>-60?'Strong':(n.rssi>-75?'Good':'Weak');opt.textContent=n.ssid+' ('+n.rssi+' dBm - '+qual+')';list.appendChild(opt);});scanCount.textContent=data.length+' APs';if(!hidden.checked)manual.value=list.value;showMsg('Found '+data.length+' Wi-Fi network(s). Select your network and enter credentials.','success');}}catch(e){scanCount.textContent='Error';showMsg('Wi-Fi scan failed: '+e.message,'error');}finally{btnScan.disabled=false;btnScan.textContent='Refresh';}}"
|
||||
"list.onchange=()=>{if(!hidden.checked)manual.value=list.value;};"
|
||||
"async function submitWifi(e){e.preventDefault();const ssidVal=manual.value.trim();const passVal=passInput.value;if(!ssidVal){showMsg('Please select or enter an SSID network name.','error');return;}btnConnect.disabled=true;btnConnect.textContent='Connecting...';showMsg('Saving credentials and connecting to \"'+ssidVal+'\"...','info');try{const body='ssid='+encodeURIComponent(ssidVal)+'&password='+encodeURIComponent(passVal);const res=await fetch('/configure',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body});if(res.ok){showMsg('Credentials saved for \"'+ssidVal+'\". ESP32 is switching to Station mode. Please connect your device to \"'+ssidVal+'\".','success');}else{const err=await res.text();showMsg('Connection failed: '+err,'error');btnConnect.disabled=false;btnConnect.textContent='Connect to Network';}}catch(e){showMsg('Credentials saved for \"'+ssidVal+'\". ESP32 is connecting to Wi-Fi. Please connect to \"'+ssidVal+'\".','success');}}"
|
||||
"if(!hidden.checked&&list.value)manual.value=list.value;"
|
||||
"scan();"
|
||||
"</script></body></html>";
|
||||
|
||||
static void copy_string(uint8_t *destination, size_t size, const char *source)
|
||||
{
|
||||
size_t length = strlen(source);
|
||||
@@ -64,9 +173,8 @@ static void append_json_string(httpd_req_t *request, const uint8_t *value, size_
|
||||
|
||||
static esp_err_t root_get(httpd_req_t *request)
|
||||
{
|
||||
const char *page = "<!doctype html><html><head><meta name=viewport content=\"width=device-width\"><title>ABrobot Wi-Fi</title><style>body{font:16px sans-serif;max-width:480px;margin:2em auto;padding:0 1em}input,select,button{font:inherit;padding:.5em;width:100%;box-sizing:border-box}button{margin-top:1em}#message{margin-top:1em}</style></head><body><h1>ABrobot Wi-Fi setup</h1><form id=form method=post action=/configure><label>Network<br><select id=networks><option value=\"\">Scanning...</option></select></label><p><label><input type=checkbox id=hidden onchange=toggleManual()> Hidden network</label></p><div id=manualBox style=display:none><label>Network name<br><input id=manual name=ssid maxlength=32></label></div><br><br><label>Password<br><input name=password type=password maxlength=64 required></label><br><br><button type=button onclick=scan()>Refresh networks</button><button type=submit>Connect</button></form><p id=message></p><script>const list=document.getElementById('networks'),manual=document.getElementById('manual'),message=document.getElementById('message');function toggleManual(){let on=hidden.checked;manualBox.style.display=on?'block':'none';manual.required=on;if(on){manual.value='';manual.focus()}else if(list.value)manual.value=list.value}form.onsubmit=()=>{if(!manual.value){message.textContent='Select a network or enable Hidden network';return false}return true};function scan(){fetch('/scan').then(r=>r.json()).then(a=>{list.innerHTML='';a.forEach(n=>{let o=document.createElement('option');o.value=n.ssid;o.textContent=n.ssid+' ('+n.rssi+' dBm)';list.appendChild(o)});if(!a.length)list.innerHTML='<option value=\"\">No networks found</option>';if(a.length&&!hidden.checked)manual.value=list.value}).catch(()=>message.textContent='Scan failed')}list.onchange=()=>{if(!hidden.checked)manual.value=list.value};scan();</script></body></html>";
|
||||
httpd_resp_set_type(request, "text/html");
|
||||
return httpd_resp_send(request, page, HTTPD_RESP_USE_STRLEN);
|
||||
return httpd_resp_send(request, WIFI_HTML, HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
|
||||
static esp_err_t scan_get(httpd_req_t *request)
|
||||
|
||||
@@ -11,11 +11,14 @@ esp_err_t web_server_start(void)
|
||||
if (s_server) return ESP_OK;
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.max_uri_handlers = 16;
|
||||
config.stack_size = 8192;
|
||||
esp_err_t err = httpd_start(&s_server, &config);
|
||||
if (err != ESP_OK) return err;
|
||||
|
||||
web_server_register_wifi_pages(s_server);
|
||||
web_server_register_ota_page(s_server);
|
||||
web_server_register_serial_pages(s_server);
|
||||
ESP_LOGI(TAG, "Web server started");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user