253 lines
17 KiB
C
253 lines
17 KiB
C
#include "web_server_pages.h"
|
|
#include "sdkconfig.h"
|
|
#include "esp_log.h"
|
|
#include "esp_wifi.h"
|
|
#include "nvs.h"
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
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;"
|
|
"try{fetch('/time/sync?epoch='+Math.floor(Date.now()/1000));}catch(e){}scan();"
|
|
"</script></body></html>";
|
|
|
|
static void copy_string(uint8_t *destination, size_t size, const char *source)
|
|
{
|
|
size_t length = strlen(source);
|
|
if (length >= size) length = size - 1;
|
|
memcpy(destination, source, length);
|
|
destination[length] = '\0';
|
|
}
|
|
|
|
static int hex_value(char value)
|
|
{
|
|
if (value >= '0' && value <= '9') return value - '0';
|
|
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
|
|
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
static bool form_value(const char *body, const char *key, char *output, size_t size)
|
|
{
|
|
size_t key_length = strlen(key);
|
|
const char *cursor = body;
|
|
size_t output_length = 0;
|
|
while (cursor && *cursor) {
|
|
if ((cursor == body || cursor[-1] == '&') && strncmp(cursor, key, key_length) == 0 && cursor[key_length] == '=') {
|
|
cursor += key_length + 1;
|
|
while (*cursor && *cursor != '&') {
|
|
int value;
|
|
if (*cursor == '+') { value = ' '; cursor++; }
|
|
else if (*cursor == '%' && hex_value(cursor[1]) >= 0 && hex_value(cursor[2]) >= 0) {
|
|
value = (hex_value(cursor[1]) << 4) | hex_value(cursor[2]); cursor += 3;
|
|
} else value = (unsigned char)*cursor++;
|
|
if (output_length + 1 >= size) return false;
|
|
output[output_length++] = (char)value;
|
|
}
|
|
output[output_length] = '\0';
|
|
return output_length > 0;
|
|
}
|
|
cursor = strchr(cursor, '&');
|
|
if (cursor) cursor++;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void append_json_string(httpd_req_t *request, const uint8_t *value, size_t length)
|
|
{
|
|
for (size_t i = 0; i < length && value[i]; i++) {
|
|
if (value[i] == '"' || value[i] == '\\') httpd_resp_send_chunk(request, "\\", 1);
|
|
char character[2] = {(char)value[i], '\0'};
|
|
httpd_resp_send_chunk(request, character, 1);
|
|
}
|
|
}
|
|
|
|
static esp_err_t root_get(httpd_req_t *request)
|
|
{
|
|
httpd_resp_set_type(request, "text/html");
|
|
return httpd_resp_send(request, WIFI_HTML, HTTPD_RESP_USE_STRLEN);
|
|
}
|
|
|
|
static esp_err_t scan_get(httpd_req_t *request)
|
|
{
|
|
wifi_scan_config_t config = {.show_hidden = true, .scan_type = WIFI_SCAN_TYPE_ACTIVE};
|
|
esp_err_t err = esp_wifi_scan_start(&config, true);
|
|
if (err != ESP_OK) return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Wi-Fi scan failed");
|
|
uint16_t count = 0;
|
|
esp_wifi_scan_get_ap_num(&count);
|
|
if (count > 32) count = 32;
|
|
wifi_ap_record_t *records = calloc(count, sizeof(wifi_ap_record_t));
|
|
if (count && !records) return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Not enough memory");
|
|
if (count && esp_wifi_scan_get_ap_records(&count, records) != ESP_OK) { free(records); return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not read scan results"); }
|
|
httpd_resp_set_type(request, "application/json");
|
|
httpd_resp_send_chunk(request, "[", 1);
|
|
uint16_t visible = 0;
|
|
for (uint16_t i = 0; i < count; i++) {
|
|
if (!records[i].ssid[0]) continue;
|
|
char prefix[32];
|
|
int length = snprintf(prefix, sizeof(prefix), "%s{\"ssid\":\"", visible ? "," : "");
|
|
httpd_resp_send_chunk(request, prefix, length);
|
|
append_json_string(request, records[i].ssid, sizeof(records[i].ssid));
|
|
char suffix[48];
|
|
length = snprintf(suffix, sizeof(suffix), "\",\"rssi\":%d}", records[i].rssi);
|
|
httpd_resp_send_chunk(request, suffix, length);
|
|
visible++;
|
|
}
|
|
httpd_resp_send_chunk(request, "]", 1);
|
|
free(records);
|
|
return httpd_resp_send_chunk(request, NULL, 0);
|
|
}
|
|
|
|
static esp_err_t save_credentials(const char *ssid, const char *password)
|
|
{
|
|
nvs_handle_t nvs;
|
|
esp_err_t err = nvs_open("wifi", NVS_READWRITE, &nvs);
|
|
if (err != ESP_OK) return err;
|
|
err = nvs_set_str(nvs, "ssid", ssid);
|
|
if (err == ESP_OK) err = nvs_set_str(nvs, "password", password);
|
|
if (err == ESP_OK) err = nvs_commit(nvs);
|
|
nvs_close(nvs);
|
|
return err;
|
|
}
|
|
|
|
static esp_err_t configure_post(httpd_req_t *request)
|
|
{
|
|
char body[160] = {0};
|
|
char ssid[sizeof(s_ssid)] = {0};
|
|
char password[sizeof(s_password)] = {0};
|
|
int received = httpd_req_recv(request, body, sizeof(body) - 1);
|
|
if (received <= 0 || !form_value(body, "ssid", ssid, sizeof(ssid)) || !form_value(body, "password", password, sizeof(password))) return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "SSID and password are required");
|
|
esp_err_t err = save_credentials(ssid, password);
|
|
if (err != ESP_OK) return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Could not save credentials");
|
|
copy_string((uint8_t *)s_ssid, sizeof(s_ssid), ssid);
|
|
copy_string((uint8_t *)s_password, sizeof(s_password), password);
|
|
httpd_resp_sendstr(request, "Credentials saved. The device is connecting; you can close this page.");
|
|
wifi_config_t config = {0};
|
|
copy_string(config.sta.ssid, sizeof(config.sta.ssid), s_ssid);
|
|
copy_string(config.sta.password, sizeof(config.sta.password), s_password);
|
|
err = esp_wifi_set_mode(WIFI_MODE_STA);
|
|
if (err == ESP_OK) err = esp_wifi_set_config(WIFI_IF_STA, &config);
|
|
if (err == ESP_OK) err = esp_wifi_connect();
|
|
if (err != ESP_OK) ESP_LOGE(TAG, "Station connection failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
void web_server_register_wifi_pages(httpd_handle_t server)
|
|
{
|
|
const httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_get};
|
|
const httpd_uri_t scan = {.uri = "/scan", .method = HTTP_GET, .handler = scan_get};
|
|
const httpd_uri_t configure = {.uri = "/configure", .method = HTTP_POST, .handler = configure_post};
|
|
httpd_register_uri_handler(server, &root);
|
|
httpd_register_uri_handler(server, &scan);
|
|
httpd_register_uri_handler(server, &configure);
|
|
}
|