#![allow(unknown_lints)]
#![allow(unexpected_cfgs)]
#[cfg(not(any(esp32h2, esp32h4, esp32p4)))]
fn main() -> anyhow::Result<()> {
example::main()
}
#[cfg(any(esp32h2, esp32h4, esp32p4))]
fn main() -> anyhow::Result<()> {
panic!("ESP32-H2, ESP32-H4 and ESP32-P4 do not have a Wifi radio (but you could enable the esp-wifi-remote component to use them with a WiFi co-processor)");
}
#[cfg(not(any(esp32h2, esp32h4, esp32p4)))]
mod example {
use core::cmp::Ordering;
use core::convert::TryInto;
use embedded_svc::{
http::Method,
io::Write,
wifi::{self, AccessPointConfiguration, AuthMethod},
ws::FrameType,
};
use esp_idf_svc::hal::peripherals::Peripherals;
use esp_idf_svc::{
eventloop::EspSystemEventLoop,
http::server::EspHttpServer,
nvs::EspDefaultNvsPartition,
systime::EspSystemTime,
wifi::{BlockingWifi, EspWifi},
};
use esp_idf_svc::sys::{EspError, ESP_ERR_INVALID_SIZE};
use log::*;
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr, str, sync::Mutex};
const SSID: &str = env!("WIFI_SSID");
const PASSWORD: &str = env!("WIFI_PASS");
static INDEX_HTML: &str = include_str!("http_ws_server_page.html");
const MAX_LEN: usize = 8;
const STACK_SIZE: usize = 10240;
const CHANNEL: u8 = 11;
struct GuessingGame {
guesses: u32,
secret: u32,
done: bool,
}
impl GuessingGame {
fn new(secret: u32) -> Self {
Self {
guesses: 0,
secret,
done: false,
}
}
fn guess(&mut self, guess: u32) -> (Ordering, u32) {
if self.done {
(Ordering::Equal, self.guesses)
} else {
self.guesses += 1;
let cmp = guess.cmp(&self.secret);
if cmp == Ordering::Equal {
self.done = true;
}
(cmp, self.guesses)
}
}
fn parse_guess(input: &str) -> Option<u32> {
let Ok(number) = input
.trim_matches(|c: char| c.is_ascii_control() || c.is_whitespace())
.parse::<u32>()
else {
warn!("Not a number: `{input}` (length {})", input.len());
return None;
};
if !(1..=100).contains(&number) {
warn!("Not in range ({number})");
return None;
}
Some(number)
}
}
fn rand() -> u32 {
EspSystemTime::now(&EspSystemTime {}).subsec_nanos() / 65537
}
fn nth(n: u32) -> Cow<'static, str> {
match n {
smaller @ (0..=13) => Cow::Borrowed(match smaller {
0 => "zeroth",
1 => "first",
2 => "second",
3 => "third",
4 => "fourth",
5 => "fifth",
6 => "sixth",
7 => "seventh",
8 => "eighth",
9 => "ninth",
10 => "10th",
11 => "11th",
12 => "12th",
13 => "13th",
_ => unreachable!(),
}),
larger => Cow::Owned(match larger % 10 {
1 => format!("{larger}st"),
2 => format!("{larger}nd"),
3 => format!("{larger}rd"),
_ => format!("{larger}th"),
}),
}
}
pub fn main() -> anyhow::Result<()> {
esp_idf_svc::sys::link_patches();
esp_idf_svc::log::EspLogger::initialize_default();
let mut server = create_server()?;
server.fn_handler("/", Method::Get, |req| {
req.into_ok_response()?
.write_all(INDEX_HTML.as_bytes())
.map(|_| ())
})?;
let guessing_games = Mutex::new(BTreeMap::<i32, GuessingGame>::new());
server.ws_handler("/ws/guess", None, move |ws| {
let mut sessions = guessing_games.lock().unwrap();
if ws.is_new() {
sessions.insert(ws.session(), GuessingGame::new((rand() % 100) + 1));
info!("New WebSocket session ({} open)", sessions.len());
ws.send(
FrameType::Text(false),
"Welcome to the guessing game! Enter a number between 1 and 100".as_bytes(),
)?;
return Ok(());
} else if ws.is_closed() {
sessions.remove(&ws.session());
info!("Closed WebSocket session ({} open)", sessions.len());
return Ok(());
}
let session = sessions.get_mut(&ws.session()).unwrap();
let (_frame_type, len) = match ws.recv(&mut []) {
Ok(frame) => frame,
Err(e) => return Err(e),
};
if len > MAX_LEN {
ws.send(FrameType::Text(false), "Request too big".as_bytes())?;
ws.send(FrameType::Close, &[])?;
return Err(EspError::from_infallible::<ESP_ERR_INVALID_SIZE>());
}
let mut buf = [0; MAX_LEN]; ws.recv(buf.as_mut())?;
let Ok(user_string) = CStr::from_bytes_until_nul(&buf[..len]) else {
ws.send(FrameType::Text(false), "[CStr decode Error]".as_bytes())?;
return Ok(());
};
let Ok(user_string) = user_string.to_str() else {
ws.send(FrameType::Text(false), "[UTF-8 Error]".as_bytes())?;
return Ok(());
};
let Some(user_guess) = GuessingGame::parse_guess(user_string) else {
ws.send(
FrameType::Text(false),
"Please enter a number between 1 and 100".as_bytes(),
)?;
return Ok(());
};
match session.guess(user_guess) {
(Ordering::Greater, n) => {
let reply = format!("Your {} guess was too high", nth(n));
ws.send(FrameType::Text(false), reply.as_ref())?;
}
(Ordering::Less, n) => {
let reply = format!("Your {} guess was too low", nth(n));
ws.send(FrameType::Text(false), reply.as_ref())?;
}
(Ordering::Equal, n) => {
let reply = format!(
"You guessed {} on your {} try! Refresh to play again",
session.secret,
nth(n)
);
ws.send(FrameType::Text(false), reply.as_ref())?;
ws.send(FrameType::Close, &[])?;
}
}
Ok::<(), EspError>(())
})?;
core::mem::forget(server);
Ok(())
}
fn create_server() -> anyhow::Result<EspHttpServer<'static>> {
let peripherals = Peripherals::take()?;
let sys_loop = EspSystemEventLoop::take()?;
let nvs = EspDefaultNvsPartition::take()?;
let mut wifi = BlockingWifi::wrap(
EspWifi::new(peripherals.modem, sys_loop.clone(), Some(nvs))?,
sys_loop,
)?;
let wifi_configuration = wifi::Configuration::AccessPoint(AccessPointConfiguration {
ssid: SSID.try_into().unwrap(),
ssid_hidden: true,
auth_method: AuthMethod::WPA2Personal,
password: PASSWORD.try_into().unwrap(),
channel: CHANNEL,
..Default::default()
});
wifi.set_configuration(&wifi_configuration)?;
wifi.start()?;
wifi.wait_netif_up()?;
info!("Created Wi-Fi with WIFI_SSID `{SSID}` and WIFI_PASS `{PASSWORD}`");
let server_configuration = esp_idf_svc::http::server::Configuration {
stack_size: STACK_SIZE,
..Default::default()
};
core::mem::forget(wifi);
Ok(EspHttpServer::new(&server_configuration)?)
}
}