Skip to content

Commit 4e61575

Browse files
authored
Add higher level server examples (#269)
* Add JSON POST handler example with buffer length detection * Add newline to HTML * Remove excessive unwraps * Add WebSocket guessing game example * Trim properly * Terminate the game when over * Document usage of core::mem::forget() * Use logical OR
1 parent 65d4fd5 commit 4e61575

File tree

5 files changed

+510
-0
lines changed

5 files changed

+510
-0
lines changed

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ anyhow = "1"
4444
anyhow = "1"
4545
esp-idf-sys = { version = "0.33", features = ["native", "binstart"] }
4646
futures = "0.3"
47+
serde = { version = "1.0", default-features = false, features = ["derive"] }
48+
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
4749

4850
[[example]]
4951
name = "http_request"
@@ -53,3 +55,9 @@ name = "wifi"
5355

5456
[[example]]
5557
name = "wifi_async"
58+
59+
[[example]]
60+
name = "json_post_handler"
61+
62+
[[example]]
63+
name = "ws_guessing_game"

examples/json_post_handler.html

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<!DOCTYPE HTML>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>ESP-RS JSON Post Handler</title>
7+
<style type="text/css">
8+
body {
9+
max-width: 50em;
10+
margin: auto;
11+
padding: 1em;
12+
font: 1em/1.65 sans-serif;
13+
}
14+
input {
15+
width: 100%;
16+
height: 3em;
17+
margin-bottom: 1em;
18+
}
19+
</style>
20+
</head>
21+
<body>
22+
<form id="the-form" action="/post" method="post" accept-charset="utf-8">
23+
<label for="first-name">First Name:</label>
24+
<input type="text" id="first-name" name="first_name"><br>
25+
<label for="age">Age:</label>
26+
<input type="number" id="age" name="age" min="0" max="150"><br>
27+
<label for="birthplace">Birthplace:</label>
28+
<input type="text" id="birthplace" name="birthplace"><br>
29+
<input type="submit" value="Submit">
30+
</form>
31+
<p id="server-resp"></p>
32+
<script type="text/javascript">
33+
34+
let theForm = document.getElementById("the-form");
35+
let serverResp = document.getElementById("server-resp");
36+
37+
theForm.addEventListener("submit", async (e) => {
38+
e.preventDefault();
39+
40+
let form = e.currentTarget;
41+
let url = form.action;
42+
43+
try {
44+
let entries = Object.fromEntries(new FormData(form).entries());
45+
entries["age"] = parseInt(entries["age"]);
46+
let resp = await fetch(url, {
47+
method: "POST",
48+
headers: {
49+
"Content-Type": "application/json",
50+
Accept: "application/json",
51+
},
52+
body: JSON.stringify(entries),
53+
});
54+
serverResp.innerText = await resp.text();
55+
} catch (err) {
56+
console.error(err);
57+
}
58+
});
59+
60+
</script>
61+
</body>
62+
</html>

examples/json_post_handler.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//! HTTP Server with JSON POST handler
2+
//!
3+
//! Go to 192.168.71.1 to test
4+
5+
use anyhow;
6+
use embedded_svc::{
7+
http::{Headers, Method},
8+
io::{Read, Write},
9+
wifi::{self, AccessPointConfiguration, AuthMethod},
10+
};
11+
use esp_idf_hal::prelude::Peripherals;
12+
use esp_idf_svc::{
13+
eventloop::EspSystemEventLoop,
14+
http::server::EspHttpServer,
15+
nvs::EspDefaultNvsPartition,
16+
wifi::{BlockingWifi, EspWifi},
17+
};
18+
use esp_idf_sys::{self as _};
19+
use log::*;
20+
use serde::Deserialize;
21+
use serde_json;
22+
23+
const SSID: &str = env!("WIFI_SSID");
24+
const PASSWORD: &str = env!("WIFI_PASS");
25+
static INDEX_HTML: &str = include_str!("json_post_handler.html");
26+
27+
// Max payload length
28+
const MAX_LEN: usize = 128;
29+
30+
// Need lots of stack to parse JSON
31+
const STACK_SIZE: usize = 10240;
32+
33+
// Wi-Fi channel, between 1 and 11
34+
const CHANNEL: u8 = 11;
35+
36+
#[derive(Deserialize)]
37+
struct FormData<'a> {
38+
first_name: &'a str,
39+
age: u32,
40+
birthplace: &'a str,
41+
}
42+
43+
fn main() -> anyhow::Result<()> {
44+
esp_idf_sys::link_patches();
45+
esp_idf_svc::log::EspLogger::initialize_default();
46+
let mut server = create_server()?;
47+
48+
server.fn_handler("/", Method::Get, |req| {
49+
req.into_ok_response()?.write(INDEX_HTML.as_bytes())?;
50+
Ok(())
51+
})?;
52+
53+
server.fn_handler("/post", Method::Post, |mut req| {
54+
let len = req.content_len().unwrap_or(0) as usize;
55+
56+
if len > MAX_LEN {
57+
req.into_status_response(413)?
58+
.write_all("Request too big".as_bytes())?;
59+
return Ok(());
60+
}
61+
62+
let mut buf = vec![0; len];
63+
req.read_exact(&mut buf)?;
64+
let mut resp = req.into_ok_response()?;
65+
66+
if let Ok(form) = serde_json::from_slice::<FormData>(&buf) {
67+
write!(
68+
resp,
69+
"Hello, {}-year-old {} from {}!",
70+
form.age, form.first_name, form.birthplace
71+
)?;
72+
} else {
73+
resp.write_all("JSON error".as_bytes())?;
74+
}
75+
76+
Ok(())
77+
})?;
78+
79+
// Keep server running beyond when main() returns (forever)
80+
// Do not call this if you ever want to stop or access it later.
81+
// Otherwise you can either add an infinite loop so the main task
82+
// never returns, or you can move it to another thread.
83+
// https://doc.rust-lang.org/stable/core/mem/fn.forget.html
84+
core::mem::forget(server);
85+
86+
// Main task no longer needed, free up some memory
87+
Ok(())
88+
}
89+
90+
fn create_server() -> anyhow::Result<EspHttpServer> {
91+
let peripherals = Peripherals::take().unwrap();
92+
let sys_loop = EspSystemEventLoop::take()?;
93+
let nvs = EspDefaultNvsPartition::take()?;
94+
95+
let mut wifi = BlockingWifi::wrap(
96+
EspWifi::new(peripherals.modem, sys_loop.clone(), Some(nvs))?,
97+
sys_loop,
98+
)?;
99+
100+
let wifi_configuration = wifi::Configuration::AccessPoint(AccessPointConfiguration {
101+
ssid: SSID.into(),
102+
ssid_hidden: true,
103+
auth_method: AuthMethod::WPA2Personal,
104+
password: PASSWORD.into(),
105+
channel: CHANNEL,
106+
..Default::default()
107+
});
108+
wifi.set_configuration(&wifi_configuration)?;
109+
wifi.start()?;
110+
wifi.wait_netif_up()?;
111+
112+
info!(
113+
"Created Wi-Fi with WIFI_SSID `{}` and WIFI_PASS `{}`",
114+
SSID, PASSWORD
115+
);
116+
117+
let server_configuration = esp_idf_svc::http::server::Configuration {
118+
stack_size: STACK_SIZE,
119+
..Default::default()
120+
};
121+
122+
// Keep wifi running beyond when this function returns (forever)
123+
// Do not call this if you ever want to stop or access it later.
124+
// Otherwise it should be returned from this function and kept somewhere
125+
// so it does not go out of scope.
126+
// https://doc.rust-lang.org/stable/core/mem/fn.forget.html
127+
core::mem::forget(wifi);
128+
129+
Ok(EspHttpServer::new(&server_configuration)?)
130+
}

examples/ws_guessing_game.html

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<!DOCTYPE HTML>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>ESP-RS WebSocket Guessing Game</title>
7+
<style type="text/css">
8+
body {
9+
max-width: 50em;
10+
margin: auto;
11+
padding: 1em;
12+
font: 1em/1.65 sans-serif;
13+
}
14+
input {
15+
width: 100%;
16+
height: 3em;
17+
margin-bottom: 1em;
18+
}
19+
</style>
20+
</head>
21+
<body onload="loadWebSocket()">
22+
<form id="the-form" action="/post" accept-charset="utf-8">
23+
<label for="user-text">Make a guess:</label>
24+
<input type="text" id="user-text" name="user_text"><br>
25+
<input type="submit" id="user-submit" value="Submit" disabled>
26+
</form>
27+
<p id="server-resp">Connecting...</p>
28+
<script type="text/javascript">
29+
30+
const submitButton = document.getElementById("user-submit");
31+
const theForm = document.getElementById("the-form");
32+
const userText = document.getElementById("user-text");
33+
const serverResp = document.getElementById("server-resp");
34+
35+
let ws;
36+
37+
function loadWebSocket() {
38+
ws = new WebSocket("ws://192.168.71.1/ws/guess");
39+
ws.onopen = function(e) {
40+
submitButton.disabled = false;
41+
};
42+
ws.onclose = ws.onerror = function(e) {
43+
submitButton.disabled = true;
44+
};
45+
ws.onmessage = function(e) {
46+
console.log(e.data);
47+
serverResp.innerText = e.data;
48+
};
49+
}
50+
51+
theForm.addEventListener("submit", async (e) => {
52+
e.preventDefault();
53+
54+
let form = e.currentTarget;
55+
let url = form.action;
56+
57+
ws.send(userText.value);
58+
});
59+
60+
</script>
61+
</body>
62+
</html>

0 commit comments

Comments
 (0)