#![allow(clippy::future_not_send, reason = "single-threaded")]
#![allow(dead_code, reason = "legacy code kept for reference")]
use defmt::{info, unwrap, warn};
use embassy_net::{Stack, tcp::TcpSocket};
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel};
use embassy_time::{Duration, Timer};
use embedded_io_async::Write;
use crate::Result;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[doc(hidden)]
pub struct WifiCredentials {
pub ssid: heapless::String<32>,
pub password: heapless::String<64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WifiCredentialSubmission {
pub credentials: WifiCredentials,
pub timezone_offset_minutes: i32,
}
static CREDENTIAL_CHANNEL: Channel<CriticalSectionRawMutex, WifiCredentialSubmission, 1> =
Channel::new();
pub async fn collect_wifi_credentials(
stack: &'static Stack<'static>,
spawner: embassy_executor::Spawner,
) -> Result<WifiCredentialSubmission> {
info!("Starting credential collection...");
let http_config_server_token = unwrap!(http_config_server_task(stack));
spawner.spawn(http_config_server_token);
info!("HTTP configuration task spawned");
info!("Waiting for user to submit credentials via web interface...");
let submission = CREDENTIAL_CHANNEL.receive().await;
info!("Credentials received!");
Ok(submission)
}
#[embassy_executor::task]
#[allow(unsafe_code)] pub async fn http_config_server_task(stack: &'static Stack<'static>) -> ! {
info!("HTTP config server starting on port 80");
static mut RX_BUFFER: [u8; 2048] = [0u8; 2048];
static mut TX_BUFFER: [u8; 4096] = [0u8; 4096];
static mut REQUEST_BUFFER: [u8; 1024] = [0u8; 1024];
let rx_buffer = unsafe { &mut *core::ptr::addr_of_mut!(RX_BUFFER) };
let tx_buffer = unsafe { &mut *core::ptr::addr_of_mut!(TX_BUFFER) };
let request = unsafe { &mut *core::ptr::addr_of_mut!(REQUEST_BUFFER) };
loop {
let mut socket = TcpSocket::new(*stack, rx_buffer, tx_buffer);
socket.set_timeout(Some(Duration::from_secs(30)));
info!("Waiting for HTTP connection...");
if let Err(e) = socket.accept(80).await {
warn!("Accept error: {:?}", e);
Timer::after_millis(500).await;
continue;
}
info!("Client connected: {:?}", socket.remote_endpoint());
let request_len = match socket.read(request).await {
Ok(0) => {
info!("Client closed without sending data");
socket.flush().await.ok();
socket.close();
continue;
}
Ok(n) => n,
Err(e) => {
warn!("Read error: {:?}", e);
socket.flush().await.ok();
socket.close();
continue;
}
};
let request_text = core::str::from_utf8(&request[..request_len]).unwrap_or("");
info!("Got HTTP request ({} bytes)", request_len);
let mut lines = request_text.lines();
let request_line = lines.next().unwrap_or("");
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or("");
let _raw_path = parts.next().unwrap_or("/");
let response = if method == "GET" {
generate_config_page()
} else if method == "POST" {
if let Some(submission) = parse_credentials_from_post(request_text) {
info!("Received WiFi credentials:");
info!(" SSID: {}", submission.credentials.ssid);
info!(" Password: [hidden]");
info!(
" Timezone offset: {} minutes",
submission.timezone_offset_minutes
);
CREDENTIAL_CHANNEL.send(submission).await;
generate_success_page()
} else {
warn!("Failed to parse credentials");
generate_error_page()
}
} else {
generate_error_page()
};
if let Err(e) = socket.write_all(response.as_bytes()).await {
warn!("Write error: {:?}", e);
}
if let Err(e) = socket.flush().await {
warn!("Flush error: {:?}", e);
}
socket.close();
Timer::after_millis(100).await;
}
}
fn parse_credentials_from_post(request: &str) -> Option<WifiCredentialSubmission> {
let body_start = request.find("\r\n\r\n")? + 4;
let body = &request[body_start..];
info!("POST body: {}", body);
let mut ssid = heapless::String::<32>::new();
let mut password = heapless::String::<64>::new();
let mut offset_minutes: Option<i32> = None;
for param in body.split('&') {
if let Some((key, value)) = param.split_once('=') {
let decoded_value = url_decode(value);
match key {
"ssid" => {
ssid.push_str(&decoded_value)
.expect("ssid exceeds capacity");
}
"password" => {
password
.push_str(&decoded_value)
.expect("password exceeds capacity");
}
"offset" => {
offset_minutes = decoded_value.parse::<i32>().ok();
}
_ => {}
}
}
}
if !ssid.is_empty() {
let timezone_offset_minutes = offset_minutes.unwrap_or(0);
Some(WifiCredentialSubmission {
credentials: WifiCredentials { ssid, password },
timezone_offset_minutes,
})
} else {
None
}
}
fn url_decode(s: &str) -> heapless::String<64> {
let mut result = heapless::String::<64>::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '+' {
result.push(' ').ok();
} else if c == '%' {
if let (Some(h1), Some(h2)) = (chars.next(), chars.next()) {
if let (Some(d1), Some(d2)) = (h1.to_digit(16), h2.to_digit(16)) {
#[allow(clippy::cast_possible_truncation)]
let byte = ((d1 << 4) | d2) as u8;
if let Ok(ch) = core::str::from_utf8(&[byte]) {
result.push_str(ch).ok();
}
}
}
} else {
result.push(c).ok();
}
}
result
}
fn generate_config_page() -> &'static str {
"HTTP/1.1 200 OK\r\n\
Content-Type: text/html\r\n\
Connection: close\r\n\
\r\n\
<!DOCTYPE html>\
<html>\
<head>\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
<title>WiFi Configuration</title>\
<style>\
body { font-family: Arial, sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; }\
h1 { color: #333; }\
form { margin-top: 20px; }\
input { width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }\
label { display: block; margin-top: 10px; }\
.toggle { display: flex; align-items: center; gap: 8px; font-size: 0.9rem; color: #444; }\
button { width: 100%; padding: 12px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }\
button:hover { background-color: #45a049; }\
</style>\
<script>\
function togglePasswordVisibility() {\
var input = document.getElementById('password');\
input.type = input.type === 'password' ? 'text' : 'password';\
}\
</script>\
</head>\
<body>\
<h1>WiFi Configuration</h1>\
<p>Enter your WiFi network credentials:</p>\
<form method=\"POST\" action=\"/\">\
<label for=\"ssid\">WiFi Network Name (SSID):</label>\
<input type=\"text\" id=\"ssid\" name=\"ssid\" required>\
<label for=\"password\">Password:</label>\
<input type=\"password\" id=\"password\" name=\"password\" required>\
<label class=\"toggle\"><input type=\"checkbox\" onclick=\"togglePasswordVisibility()\">Show password</label>\
<label for=\"offset\">Timezone:</label>\
<select id=\"offset\" name=\"offset\">\
<option value=\"-720\">Baker Island (UTC-12:00)</option>\
<option value=\"-660\">American Samoa (UTC-11:00)</option>\
<option value=\"-600\">Honolulu (UTC-10:00)</option>\
<option value=\"-540\">Anchorage, Alaska ST (UTC-09:00)</option>\
<option value=\"-480\">Anchorage, Alaska DT (UTC-08:00)</option>\
<option value=\"-480\">Los Angeles, San Francisco, Seattle ST (UTC-08:00)</option>\
<option value=\"-420\">Los Angeles, San Francisco, Seattle DT (UTC-07:00)</option>\
<option value=\"-420\">Denver, Phoenix ST (UTC-07:00)</option>\
<option value=\"-360\">Denver DT (UTC-06:00)</option>\
<option value=\"-360\">Chicago, Dallas, Mexico City ST (UTC-06:00)</option>\
<option value=\"-300\">Chicago, Dallas DT (UTC-05:00)</option>\
<option value=\"-300\">New York, Toronto, Bogota ST (UTC-05:00)</option>\
<option value=\"-240\">New York, Toronto DT (UTC-04:00)</option>\
<option value=\"-240\">Santiago, Halifax ST (UTC-04:00)</option>\
<option value=\"-210\">St. John's, Newfoundland ST (UTC-03:30)</option>\
<option value=\"-180\">Buenos Aires, Sao Paulo (UTC-03:00)</option>\
<option value=\"-120\">South Georgia (UTC-02:00)</option>\
<option value=\"-60\">Azores ST (UTC-01:00)</option>\
<option value=\"0\" selected>London, Lisbon ST (UTC+00:00)</option>\
<option value=\"60\">London, Paris, Berlin DT (UTC+01:00)</option>\
<option value=\"60\">Paris, Berlin, Rome ST (UTC+01:00)</option>\
<option value=\"120\">Paris, Berlin, Rome DT (UTC+02:00)</option>\
<option value=\"120\">Athens, Cairo, Johannesburg ST (UTC+02:00)</option>\
<option value=\"180\">Athens DT (UTC+03:00)</option>\
<option value=\"180\">Moscow, Istanbul, Nairobi (UTC+03:00)</option>\
<option value=\"240\">Dubai, Baku (UTC+04:00)</option>\
<option value=\"270\">Tehran ST (UTC+04:30)</option>\
<option value=\"300\">Karachi, Tashkent (UTC+05:00)</option>\
<option value=\"330\">Mumbai, Delhi (UTC+05:30)</option>\
<option value=\"345\">Kathmandu (UTC+05:45)</option>\
<option value=\"360\">Dhaka, Almaty (UTC+06:00)</option>\
<option value=\"390\">Yangon (UTC+06:30)</option>\
<option value=\"420\">Bangkok, Jakarta (UTC+07:00)</option>\
<option value=\"480\">Singapore, Hong Kong, Beijing (UTC+08:00)</option>\
<option value=\"525\">Eucla, Australia (UTC+08:45)</option>\
<option value=\"540\">Tokyo, Seoul (UTC+09:00)</option>\
<option value=\"570\">Adelaide ST (UTC+09:30)</option>\
<option value=\"600\">Sydney, Melbourne ST (UTC+10:00)</option>\
<option value=\"630\">Adelaide DT (UTC+10:30)</option>\
<option value=\"660\">Sydney, Melbourne DT (UTC+11:00)</option>\
<option value=\"720\">Auckland, Fiji ST (UTC+12:00)</option>\
<option value=\"780\">Auckland DT (UTC+13:00)</option>\
<option value=\"840\">Kiribati (UTC+14:00)</option>\
</select>\
<button type=\"submit\">Connect</button>\
</form>\
</body>\
</html>"
}
fn generate_success_page() -> &'static str {
"HTTP/1.1 200 OK\r\n\
Content-Type: text/html\r\n\
Connection: close\r\n\
\r\n\
<!DOCTYPE html>\
<html>\
<head>\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
<title>Configuration Saved</title>\
<style>\
body { font-family: Arial, sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; text-align: center; }\
h1 { color: #4CAF50; }\
</style>\
</head>\
<body>\
<h1>Configuration Saved!</h1>\
<p>WiFi credentials have been received.</p>\
<p>The device will restart and connect to your network.</p>\
<p>You can close this page.</p>\
</body>\
</html>"
}
fn generate_error_page() -> &'static str {
"HTTP/1.1 400 Bad Request\r\n\
Content-Type: text/html\r\n\
Connection: close\r\n\
\r\n\
<!DOCTYPE html>\
<html>\
<head>\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\
<title>Error</title>\
<style>\
body { font-family: Arial, sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; text-align: center; }\
h1 { color: #f44336; }\
</style>\
</head>\
<body>\
<h1>Error</h1>\
<p>Failed to process your request.</p>\
<p><a href=\"/\">Try again</a></p>\
</body>\
</html>"
}