esp32: wifi provisioning via AP captive portal
Replace compile-time env!() wifi/server config with NVS-based runtime provisioning. Boot checks NVS — if no config, starts AP mode (KFrame-Setup) with DNS responder + HTTP config form. WiFi failure clears config and reboots into setup mode.
This commit is contained in:
@@ -2,8 +2,6 @@ use esp_idf_hal::delay::{Delay, Ets};
|
|||||||
use esp_idf_hal::gpio::{AnyIOPin, AnyOutputPin, PinDriver};
|
use esp_idf_hal::gpio::{AnyIOPin, AnyOutputPin, PinDriver};
|
||||||
use esp_idf_hal::spi::{SpiDeviceDriver, SpiDriver, SpiDriverConfig, SPI2, config::Config as SpiConfig};
|
use esp_idf_hal::spi::{SpiDeviceDriver, SpiDriver, SpiDriverConfig, SPI2, config::Config as SpiConfig};
|
||||||
use mipidsi::{Builder, models::ILI9341Rgb565, options::{ColorOrder, Orientation, Rotation}, interface::SpiInterface};
|
use mipidsi::{Builder, models::ILI9341Rgb565, options::{ColorOrder, Orientation, Rotation}, interface::SpiInterface};
|
||||||
use embedded_graphics::pixelcolor::Rgb565;
|
|
||||||
use embedded_graphics::prelude::*;
|
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::config::{self, SPI_BAUDRATE, SPI_BUFFER_SIZE};
|
use crate::config::{self, SPI_BAUDRATE, SPI_BUFFER_SIZE};
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ use esp_idf_hal::modem::Modem;
|
|||||||
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
use esp_idf_svc::eventloop::EspSystemEventLoop;
|
||||||
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
||||||
use esp_idf_svc::wifi::{
|
use esp_idf_svc::wifi::{
|
||||||
AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi,
|
AccessPointConfiguration, AuthMethod, BlockingWifi, ClientConfiguration,
|
||||||
|
Configuration, EspWifi,
|
||||||
};
|
};
|
||||||
use log::{info, error};
|
use log::{info, error};
|
||||||
|
|
||||||
const MAX_RETRIES: u32 = 5;
|
const MAX_RETRIES: u32 = 5;
|
||||||
const RETRY_DELAY: Duration = Duration::from_secs(3);
|
const RETRY_DELAY: Duration = Duration::from_secs(3);
|
||||||
|
const AP_SSID: &str = "KFrame-Setup";
|
||||||
|
const AP_CHANNEL: u8 = 1;
|
||||||
|
const AP_MAX_CONNECTIONS: u16 = 4;
|
||||||
|
|
||||||
pub fn init<'d>(
|
pub fn init<'d>(
|
||||||
modem: Modem<'d>,
|
modem: Modem<'d>,
|
||||||
@@ -54,3 +58,29 @@ pub fn init<'d>(
|
|||||||
|
|
||||||
Err(format!("WiFi failed after {MAX_RETRIES} attempts"))
|
Err(format!("WiFi failed after {MAX_RETRIES} attempts"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn init_ap<'d>(
|
||||||
|
modem: Modem<'d>,
|
||||||
|
sysloop: EspSystemEventLoop,
|
||||||
|
nvs: EspDefaultNvsPartition,
|
||||||
|
) -> Result<BlockingWifi<EspWifi<'d>>, String> {
|
||||||
|
let esp_wifi = EspWifi::new(modem, sysloop.clone(), Some(nvs))
|
||||||
|
.map_err(|e| format!("wifi new: {e:?}"))?;
|
||||||
|
|
||||||
|
let mut wifi = BlockingWifi::wrap(esp_wifi, sysloop)
|
||||||
|
.map_err(|e| format!("wifi wrap: {e:?}"))?;
|
||||||
|
|
||||||
|
let config = Configuration::AccessPoint(AccessPointConfiguration {
|
||||||
|
ssid: AP_SSID.try_into().unwrap(),
|
||||||
|
auth_method: AuthMethod::None,
|
||||||
|
channel: AP_CHANNEL,
|
||||||
|
max_connections: AP_MAX_CONNECTIONS,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
wifi.set_configuration(&config).map_err(|e| format!("wifi ap config: {e:?}"))?;
|
||||||
|
wifi.start().map_err(|e| format!("wifi ap start: {e:?}"))?;
|
||||||
|
|
||||||
|
info!("AP started: SSID={AP_SSID}");
|
||||||
|
Ok(wifi)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ mod adapters;
|
|||||||
mod boot;
|
mod boot;
|
||||||
mod config;
|
mod config;
|
||||||
mod hal;
|
mod hal;
|
||||||
|
mod provisioning;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
|
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
@@ -10,10 +11,6 @@ use esp_idf_svc::eventloop::EspSystemEventLoop;
|
|||||||
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
use esp_idf_svc::nvs::EspDefaultNvsPartition;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
const WIFI_SSID: &str = env!("KFRAME_WIFI_SSID");
|
|
||||||
const WIFI_PASS: &str = env!("KFRAME_WIFI_PASS");
|
|
||||||
const SERVER_ADDR: &str = env!("KFRAME_SERVER_ADDR");
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
esp_idf_svc::sys::link_patches();
|
esp_idf_svc::sys::link_patches();
|
||||||
esp_idf_svc::log::EspLogger::initialize_default();
|
esp_idf_svc::log::EspLogger::initialize_default();
|
||||||
@@ -24,7 +21,7 @@ fn main() {
|
|||||||
let sysloop = EspSystemEventLoop::take().unwrap();
|
let sysloop = EspSystemEventLoop::take().unwrap();
|
||||||
let nvs = EspDefaultNvsPartition::take().unwrap();
|
let nvs = EspDefaultNvsPartition::take().unwrap();
|
||||||
|
|
||||||
let display = hal::display::init(hal::display::DisplayHardware {
|
let mut display = hal::display::init(hal::display::DisplayHardware {
|
||||||
spi: peripherals.spi2,
|
spi: peripherals.spi2,
|
||||||
sclk: peripherals.pins.gpio18.into(),
|
sclk: peripherals.pins.gpio18.into(),
|
||||||
mosi: peripherals.pins.gpio23.into(),
|
mosi: peripherals.pins.gpio23.into(),
|
||||||
@@ -34,11 +31,46 @@ fn main() {
|
|||||||
});
|
});
|
||||||
info!("Display ready");
|
info!("Display ready");
|
||||||
|
|
||||||
info!("Connecting WiFi...");
|
match provisioning::read_config(nvs.clone()) {
|
||||||
let _wifi = hal::wifi::init(peripherals.modem, sysloop, nvs, WIFI_SSID, WIFI_PASS)
|
Some(cfg) => run_station(peripherals.modem, sysloop, nvs, cfg, display),
|
||||||
.expect("WiFi init failed");
|
None => {
|
||||||
|
info!("No config found, entering setup mode");
|
||||||
|
run_setup(peripherals.modem, sysloop, nvs, &mut display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_station(
|
||||||
|
modem: esp_idf_hal::modem::Modem<'static>,
|
||||||
|
sysloop: EspSystemEventLoop,
|
||||||
|
nvs: EspDefaultNvsPartition,
|
||||||
|
cfg: provisioning::DeviceConfig,
|
||||||
|
display: adapters::display::Esp32DisplayAdapter,
|
||||||
|
) {
|
||||||
|
info!("Connecting WiFi...");
|
||||||
|
match hal::wifi::init(modem, sysloop.clone(), nvs.clone(), &cfg.wifi_ssid, &cfg.wifi_pass) {
|
||||||
|
Ok(_wifi) => {
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
tasks::network::spawn(SERVER_ADDR.into(), tx);
|
tasks::network::spawn(cfg.server_addr, tx);
|
||||||
tasks::render::run(config::SCREEN, display, rx);
|
tasks::render::run(config::SCREEN, display, rx);
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
info!("WiFi failed ({e}), clearing config and rebooting to setup mode");
|
||||||
|
provisioning::clear_config(nvs);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
unsafe { esp_idf_svc::sys::esp_restart(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_setup(
|
||||||
|
modem: esp_idf_hal::modem::Modem<'static>,
|
||||||
|
sysloop: EspSystemEventLoop,
|
||||||
|
nvs: EspDefaultNvsPartition,
|
||||||
|
display: &mut adapters::display::Esp32DisplayAdapter,
|
||||||
|
) {
|
||||||
|
let _wifi = hal::wifi::init_ap(modem, sysloop, nvs.clone())
|
||||||
|
.expect("AP mode failed");
|
||||||
|
|
||||||
|
provisioning::portal::run_captive_portal(nvs, display);
|
||||||
|
}
|
||||||
|
|||||||
48
crates/client-esp32/src/provisioning/html.rs
Normal file
48
crates/client-esp32/src/provisioning/html.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
pub const SETUP_HTML: &str = r#"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>K-Frame Setup</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:system-ui,sans-serif;background:#1a1a2e;color:#e0e0e0;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:16px}
|
||||||
|
.card{background:#16213e;border-radius:12px;padding:24px;width:100%;max-width:360px}
|
||||||
|
h1{font-size:20px;text-align:center;margin-bottom:20px;color:#e94560}
|
||||||
|
label{display:block;font-size:13px;margin-bottom:4px;color:#a0a0a0}
|
||||||
|
input{width:100%;padding:10px;margin-bottom:14px;border:1px solid #333;border-radius:6px;background:#0f3460;color:#fff;font-size:15px}
|
||||||
|
input:focus{outline:none;border-color:#e94560}
|
||||||
|
button{width:100%;padding:12px;background:#e94560;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer}
|
||||||
|
button:active{background:#c73650}
|
||||||
|
.ok{text-align:center;color:#4ecca3;margin-top:16px;display:none}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>K-Frame Setup</h1>
|
||||||
|
<form id="f" method="POST" action="/save">
|
||||||
|
<label for="s">WiFi SSID</label>
|
||||||
|
<input id="s" name="ssid" required autocomplete="off">
|
||||||
|
<label for="p">WiFi Password</label>
|
||||||
|
<input id="p" name="pass" type="password">
|
||||||
|
<label for="a">Server Address</label>
|
||||||
|
<input id="a" name="server" placeholder="192.168.x.x:2699" required>
|
||||||
|
<button type="submit">Save & Reboot</button>
|
||||||
|
</form>
|
||||||
|
<div class="ok" id="ok">Saved! Rebooting...</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.getElementById('f').onsubmit=function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
var d=new FormData(this);
|
||||||
|
var x=new XMLHttpRequest();
|
||||||
|
x.open('POST','/save');
|
||||||
|
x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
|
||||||
|
x.onload=function(){
|
||||||
|
document.getElementById('f').style.display='none';
|
||||||
|
document.getElementById('ok').style.display='block';
|
||||||
|
};
|
||||||
|
x.send('ssid='+encodeURIComponent(d.get('ssid'))+'&pass='+encodeURIComponent(d.get('pass'))+'&server='+encodeURIComponent(d.get('server')));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>"#;
|
||||||
65
crates/client-esp32/src/provisioning/mod.rs
Normal file
65
crates/client-esp32/src/provisioning/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
pub mod html;
|
||||||
|
pub mod portal;
|
||||||
|
|
||||||
|
use esp_idf_svc::nvs::{EspNvs, EspNvsPartition, NvsDefault};
|
||||||
|
use log::{info, error};
|
||||||
|
|
||||||
|
const NVS_NAMESPACE: &str = "kframe";
|
||||||
|
const KEY_SSID: &str = "wifi_ssid";
|
||||||
|
const KEY_PASS: &str = "wifi_pass";
|
||||||
|
const KEY_SERVER: &str = "server_addr";
|
||||||
|
|
||||||
|
pub struct DeviceConfig {
|
||||||
|
pub wifi_ssid: String,
|
||||||
|
pub wifi_pass: String,
|
||||||
|
pub server_addr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_config(nvs_partition: EspNvsPartition<NvsDefault>) -> Option<DeviceConfig> {
|
||||||
|
let nvs = EspNvs::new(nvs_partition, NVS_NAMESPACE, true).ok()?;
|
||||||
|
|
||||||
|
let ssid = read_string(&nvs, KEY_SSID)?;
|
||||||
|
let pass = read_string(&nvs, KEY_PASS)?;
|
||||||
|
let server = read_string(&nvs, KEY_SERVER)?;
|
||||||
|
|
||||||
|
if ssid.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("NVS config found: ssid={ssid}, server={server}");
|
||||||
|
Some(DeviceConfig {
|
||||||
|
wifi_ssid: ssid,
|
||||||
|
wifi_pass: pass,
|
||||||
|
server_addr: server,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_config(nvs_partition: EspNvsPartition<NvsDefault>, config: &DeviceConfig) -> Result<(), String> {
|
||||||
|
let nvs = EspNvs::new(nvs_partition, NVS_NAMESPACE, true)
|
||||||
|
.map_err(|e| format!("nvs open: {e:?}"))?;
|
||||||
|
|
||||||
|
nvs.set_str(KEY_SSID, &config.wifi_ssid).map_err(|e| format!("nvs set ssid: {e:?}"))?;
|
||||||
|
nvs.set_str(KEY_PASS, &config.wifi_pass).map_err(|e| format!("nvs set pass: {e:?}"))?;
|
||||||
|
nvs.set_str(KEY_SERVER, &config.server_addr).map_err(|e| format!("nvs set server: {e:?}"))?;
|
||||||
|
|
||||||
|
info!("Config saved to NVS");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_config(nvs_partition: EspNvsPartition<NvsDefault>) {
|
||||||
|
match EspNvs::new(nvs_partition, NVS_NAMESPACE, true) {
|
||||||
|
Ok(nvs) => {
|
||||||
|
let _ = nvs.remove(KEY_SSID);
|
||||||
|
let _ = nvs.remove(KEY_PASS);
|
||||||
|
let _ = nvs.remove(KEY_SERVER);
|
||||||
|
info!("NVS config cleared");
|
||||||
|
}
|
||||||
|
Err(e) => error!("Failed to clear NVS: {e:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_string(nvs: &EspNvs<NvsDefault>, key: &str) -> Option<String> {
|
||||||
|
let len = nvs.str_len(key).ok()??;
|
||||||
|
let mut buf = vec![0u8; len];
|
||||||
|
nvs.get_str(key, &mut buf).ok()?.map(|s| s.to_string())
|
||||||
|
}
|
||||||
201
crates/client-esp32/src/provisioning/portal.rs
Normal file
201
crates/client-esp32/src/provisioning/portal.rs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
use std::net::UdpSocket;
|
||||||
|
use std::thread;
|
||||||
|
use esp_idf_hal::io::Write;
|
||||||
|
use esp_idf_svc::http::server::{Configuration as HttpConfig, EspHttpServer};
|
||||||
|
use esp_idf_svc::nvs::{EspNvsPartition, NvsDefault};
|
||||||
|
use client_domain::{BoundingBox, DisplayPort};
|
||||||
|
use log::{info, error};
|
||||||
|
|
||||||
|
use super::{DeviceConfig, save_config};
|
||||||
|
use super::html::SETUP_HTML;
|
||||||
|
|
||||||
|
const AP_IP: [u8; 4] = [192, 168, 4, 1];
|
||||||
|
|
||||||
|
pub fn run_captive_portal<D: DisplayPort>(
|
||||||
|
nvs: EspNvsPartition<NvsDefault>,
|
||||||
|
display: &mut D,
|
||||||
|
) {
|
||||||
|
draw_setup_screen(display);
|
||||||
|
spawn_dns_responder();
|
||||||
|
|
||||||
|
let nvs_clone = nvs.clone();
|
||||||
|
let mut server = EspHttpServer::new(&HttpConfig {
|
||||||
|
http_port: 80,
|
||||||
|
..Default::default()
|
||||||
|
}).expect("HTTP server start failed");
|
||||||
|
|
||||||
|
server
|
||||||
|
.fn_handler::<esp_idf_svc::io::EspIOError, _>("/", esp_idf_svc::http::Method::Get, |req| {
|
||||||
|
req.into_ok_response()?
|
||||||
|
.write_all(SETUP_HTML.as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.expect("GET / handler failed");
|
||||||
|
|
||||||
|
server
|
||||||
|
.fn_handler::<esp_idf_svc::io::EspIOError, _>("/save", esp_idf_svc::http::Method::Post, move |mut req| {
|
||||||
|
let mut body = vec![0u8; 512];
|
||||||
|
let len = req.read(&mut body).unwrap_or(0);
|
||||||
|
let body_str = std::str::from_utf8(&body[..len]).unwrap_or("");
|
||||||
|
|
||||||
|
let config = parse_form(body_str);
|
||||||
|
info!("Portal received config: ssid={}", config.wifi_ssid);
|
||||||
|
|
||||||
|
if let Err(e) = save_config(nvs_clone.clone(), &config) {
|
||||||
|
error!("Save failed: {e}");
|
||||||
|
req.into_ok_response()?.write_all(b"Error saving config")?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
req.into_ok_response()?.write_all(b"OK")?;
|
||||||
|
|
||||||
|
thread::spawn(|| {
|
||||||
|
thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
unsafe { esp_idf_svc::sys::esp_restart(); }
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.expect("POST /save handler failed");
|
||||||
|
|
||||||
|
server
|
||||||
|
.fn_handler::<esp_idf_svc::io::EspIOError, _>("/generate_204", esp_idf_svc::http::Method::Get, |req| {
|
||||||
|
redirect(req)
|
||||||
|
})
|
||||||
|
.expect("generate_204 handler failed");
|
||||||
|
|
||||||
|
server
|
||||||
|
.fn_handler::<esp_idf_svc::io::EspIOError, _>("/hotspot-detect.html", esp_idf_svc::http::Method::Get, |req| {
|
||||||
|
redirect(req)
|
||||||
|
})
|
||||||
|
.expect("hotspot-detect handler failed");
|
||||||
|
|
||||||
|
server
|
||||||
|
.fn_handler::<esp_idf_svc::io::EspIOError, _>("/canonical.html", esp_idf_svc::http::Method::Get, |req| {
|
||||||
|
redirect(req)
|
||||||
|
})
|
||||||
|
.expect("canonical handler failed");
|
||||||
|
|
||||||
|
info!("Captive portal running on http://192.168.4.1");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
thread::sleep(std::time::Duration::from_secs(60));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redirect(req: esp_idf_svc::http::server::Request<&mut esp_idf_svc::http::server::EspHttpConnection>) -> Result<(), esp_idf_svc::io::EspIOError> {
|
||||||
|
let mut resp = req.into_response(302, None, &[("Location", "http://192.168.4.1/")])?;
|
||||||
|
resp.write_all(b"")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_dns_responder() {
|
||||||
|
thread::Builder::new()
|
||||||
|
.stack_size(4096)
|
||||||
|
.name("dns".into())
|
||||||
|
.spawn(dns_responder)
|
||||||
|
.expect("DNS thread spawn failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dns_responder() {
|
||||||
|
let socket = match UdpSocket::bind("0.0.0.0:53") {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
error!("DNS bind failed: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("DNS responder started on :53");
|
||||||
|
let mut buf = [0u8; 512];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (len, src) = match socket.recv_from(&mut buf) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if len < 12 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut resp = Vec::with_capacity(len + 16);
|
||||||
|
|
||||||
|
// Header: copy ID, set response flags
|
||||||
|
resp.extend_from_slice(&buf[0..2]);
|
||||||
|
resp.extend_from_slice(&[0x81, 0x80]);
|
||||||
|
resp.extend_from_slice(&buf[4..6]);
|
||||||
|
resp.extend_from_slice(&buf[4..6]);
|
||||||
|
resp.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
|
||||||
|
// Copy question section
|
||||||
|
resp.extend_from_slice(&buf[12..len]);
|
||||||
|
|
||||||
|
// Answer: A record pointing to AP IP
|
||||||
|
resp.extend_from_slice(&[0xC0, 0x0C]); // Name pointer
|
||||||
|
resp.extend_from_slice(&[0, 1]); // Type A
|
||||||
|
resp.extend_from_slice(&[0, 1]); // Class IN
|
||||||
|
resp.extend_from_slice(&[0, 0, 0, 60]); // TTL 60s
|
||||||
|
resp.extend_from_slice(&[0, 4]); // Data length
|
||||||
|
resp.extend_from_slice(&AP_IP);
|
||||||
|
|
||||||
|
let _ = socket.send_to(&resp, src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_form(body: &str) -> DeviceConfig {
|
||||||
|
let mut ssid = String::new();
|
||||||
|
let mut pass = String::new();
|
||||||
|
let mut server = String::new();
|
||||||
|
|
||||||
|
for pair in body.split('&') {
|
||||||
|
let mut parts = pair.splitn(2, '=');
|
||||||
|
let key = parts.next().unwrap_or("");
|
||||||
|
let val = url_decode(parts.next().unwrap_or(""));
|
||||||
|
match key {
|
||||||
|
"ssid" => ssid = val,
|
||||||
|
"pass" => pass = val,
|
||||||
|
"server" => server = val,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceConfig { wifi_ssid: ssid, wifi_pass: pass, server_addr: server }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url_decode(s: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(s.len());
|
||||||
|
let mut chars = s.bytes();
|
||||||
|
while let Some(b) = chars.next() {
|
||||||
|
match b {
|
||||||
|
b'+' => result.push(' '),
|
||||||
|
b'%' => {
|
||||||
|
let hi = chars.next().and_then(hex_val);
|
||||||
|
let lo = chars.next().and_then(hex_val);
|
||||||
|
if let (Some(h), Some(l)) = (hi, lo) {
|
||||||
|
result.push((h << 4 | l) as char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => result.push(b as char),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_val(b: u8) -> Option<u8> {
|
||||||
|
match b {
|
||||||
|
b'0'..=b'9' => Some(b - b'0'),
|
||||||
|
b'a'..=b'f' => Some(b - b'a' + 10),
|
||||||
|
b'A'..=b'F' => Some(b - b'A' + 10),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FULL_SCREEN: BoundingBox = BoundingBox { x: 0, y: 0, width: 320, height: 240 };
|
||||||
|
|
||||||
|
fn draw_setup_screen<D: DisplayPort>(display: &mut D) {
|
||||||
|
let _ = display.fill_background(FULL_SCREEN);
|
||||||
|
let _ = display.draw_text("K-Frame Setup", 0, 0, BoundingBox { x: 80, y: 50, width: 160, height: 20 });
|
||||||
|
let _ = display.draw_text("Connect to WiFi:", 0, 0, BoundingBox { x: 40, y: 90, width: 240, height: 14 });
|
||||||
|
let _ = display.draw_text("KFrame-Setup", 0, 0, BoundingBox { x: 80, y: 110, width: 160, height: 14 });
|
||||||
|
let _ = display.draw_text("Then open browser", 0, 0, BoundingBox { x: 40, y: 150, width: 240, height: 14 });
|
||||||
|
let _ = display.draw_text("to configure", 0, 0, BoundingBox { x: 60, y: 170, width: 200, height: 14 });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user