Skip to content

Commit

Permalink
instructions for module approach
Browse files Browse the repository at this point in the history
  • Loading branch information
ImplFerris committed Jan 11, 2025
1 parent 22985b9 commit cae6a77
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 134 deletions.
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
- [Connecting Wifi](./wifi/embassy/connecting-wifi.md)
- [HTTP Request](./wifi/embassy/http-request.md)
- [Webserver on ESP32](./wifi/web-server/index.md)
- [Wi-Fi module](./wifi/web-server/wifi.md)
- [Serve Webpage](./wifi/web-server/serve-website.md)
- [Exposing to Internet](./wifi/web-server/exposing-to-internet.md)
- [Static IP](./wifi/static-ip.md)
Expand Down
2 changes: 2 additions & 0 deletions src/wifi/access-point/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

We need to pass the environment variables for the Wi-Fi connection.

Note: This time, you need to provide the Wi-Fi name and password for the network we will create with the ESP32. You can choose any name up to 32 characters and pass a password (or skip it if you commented out the password section in the code).

```sh
SSID=YOUR_WIFI_NAME PASSWORD=YOUR_WIFI_PASSWORD cargo run --release
```
Expand Down
165 changes: 59 additions & 106 deletions src/wifi/web-server/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,35 @@ embassy-net = { version = "0.5.0", features = [
] }
```

## Project Structure

## Wi-Fi connection setup
We will organize the logic by splitting it into modules. Under the lib, we will create two submodules: web.rs and wifi.rs.

The Wi-Fi setup code is the same as explained in the ["Connecting Wi-Fi with Embassy"](../embassy/connecting-wifi.md) chapter on the Access Website. To avoid repetition, I won't explain it again here. Please refer to that chapter if you haven't already.
```
├── build.rs
├── Cargo.toml
├── rust-toolchain.toml
├── src
│ ├── bin
│ │ └── async_main.rs
│ ├── index.html
│ ├── lib.rs
│ ├── web.rs
│ └── wifi.rs
```

## Lib Module

We'll relocate the mk_static macro, which allows creating static variables initialized at runtime, into the lib module. Additionally, we'll enable the impl_trait_in_assoc_type feature, as it is required by the picoserve crate.

```rust
const SSID: &str = env!("SSID");
const PASSWORD: &str = env!("PASSWORD");
#![no_std]
#![feature(impl_trait_in_assoc_type)]

pub mod web;
pub mod wifi;

#[macro_export]
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
Expand All @@ -78,123 +98,56 @@ macro_rules! mk_static {
}
```

In the main function, we have included boilerplate code to set up the global heap allocator and initialize Embassy. After that, we have created a Wi-Fi controller and run the network stack along with Wi-Fi connection monitoring tasks in the background. Finally, we print the IP address assigned to our ESP32 by DHCP.

```rust
let peripherals = esp_hal::init({
let mut config = esp_hal::Config::default();
config.cpu_clock = CpuClock::max();
config
});

esp_alloc::heap_allocator!(72 * 1024);

esp_println::logger::init_logger_from_env();

let timer0 = esp_hal::timer::timg::TimerGroup::new(peripherals.TIMG1);
esp_hal_embassy::init(timer0.timer0);

let mut rng = Rng::new(peripherals.RNG);
let net_seed = rng.random() as u64 | ((rng.random() as u64) << 32);
## The main function (async_main.rs file)

info!("Embassy initialized!");
To use the lib module, we would normally have to reference it using the full project name (e.g., webserver::web). However, to keep the references consistent across different exercises, we will alias the import as "lib". This allows us to use it as "lib::web" instead.

let timg0 = esp_hal::timer::timg::TimerGroup::new(peripherals.TIMG0);
In the main function, we start with some boilerplate code to set up the global heap allocator and initialize Embassy.

let wifi_init = &*mk_static!(
EspWifiController<'static>,
esp_wifi::init(timg0.timer0, rng.clone(), peripherals.RADIO_CLK).unwrap()
);
Next, we create a Wi-Fi controller, which we will pass to the start_wifi function; that we will soon define in the wifi module. This function will return the network stack instance.

let wifi = peripherals.WIFI;
We will create a web application instance, configure routing and settings using the picoserve crate. We will then spawn multiple tasks to handle incoming requests based on the defined pool size. Each task receives the task ID, app instance, network stack, and server settings.

let (wifi_interface, controller) =
esp_wifi::wifi::new_with_mode(&wifi_init, wifi, WifiStaDevice).unwrap();
```rust
use webserver as lib;

let dhcp_config = DhcpConfig::default();
#[main]
async fn main(spawner: Spawner) {
let peripherals = esp_hal::init({
let mut config = esp_hal::Config::default();
config.cpu_clock = CpuClock::max();
config
});

let net_config = embassy_net::Config::dhcpv4(dhcp_config);
esp_alloc::heap_allocator!(72 * 1024);

// This part different from previous:
let (stack, runner) = mk_static!(
(
Stack<'static>,
Runner<'static, WifiDevice<'static, WifiStaDevice>>
),
embassy_net::new(
wifi_interface,
net_config,
mk_static!(StackResources<3>, StackResources::<3>::new()),
net_seed
)
);
esp_println::logger::init_logger_from_env();

spawner.spawn(connection_task(controller)).ok();
spawner.spawn(net_task(runner)).ok();
let timer0 = esp_hal::timer::timg::TimerGroup::new(peripherals.TIMG1);
esp_hal_embassy::init(timer0.timer0);

loop {
if stack.is_link_up() {
break;
}
Timer::after(Duration::from_millis(500)).await;
}

println!("Waiting to get IP address...");
loop {
if let Some(config) = stack.config_v4() {
println!("Got IP: {}", config.address);
break;
}
Timer::after(Duration::from_millis(500)).await;
}
let rng = Rng::new(peripherals.RNG);

```
info!("Embassy initialized!");

The code is almost the same as what we did earlier, with a small change. In the version of embassy-net we are using, we have to initialize the network stack by calling `embassy_net::new`, which returns a tuple containing the stack and runner instances.
let timg0 = esp_hal::timer::timg::TimerGroup::new(peripherals.TIMG0);

### Wi-Fi and Network Tasks
let wifi_init = lib::mk_static!(
EspWifiController<'static>,
esp_wifi::init(timg0.timer0, rng, peripherals.RADIO_CLK).unwrap()
);

There is no major change in the logic of these two tasks. The only difference is that we are now passing the runner instance to the net_task, unlike before.
let stack = lib::wifi::start_wifi(wifi_init, peripherals.WIFI, rng, &spawner).await;

```rust
#[embassy_executor::task]
async fn connection_task(mut controller: WifiController<'static>) {
println!("start connection task");
println!("Device capabilities: {:?}", controller.capabilities());
loop {
match esp_wifi::wifi::wifi_state() {
WifiState::StaConnected => {
// wait until we're no longer connected
controller.wait_for_event(WifiEvent::StaDisconnected).await;
Timer::after(Duration::from_millis(5000)).await
}
_ => {}
}
if !matches!(controller.is_started(), Ok(true)) {
let client_config = Configuration::Client(ClientConfiguration {
ssid: SSID.try_into().unwrap(),
password: PASSWORD.try_into().unwrap(),
..Default::default()
});
controller.set_configuration(&client_config).unwrap();
println!("Starting wifi");
controller.start_async().await.unwrap();
println!("Wifi started!");
}
println!("About to connect...");

match controller.connect_async().await {
Ok(_) => println!("Wifi connected!"),
Err(e) => {
println!("Failed to connect to wifi: {e:?}");
Timer::after(Duration::from_millis(5000)).await
}
}
let web_app = lib::web::WebApp::default();
for id in 0..lib::web::WEB_TASK_POOL_SIZE {
spawner.must_spawn(lib::web::web_task(
id,
*stack,
web_app.router,
web_app.config,
));
}
}

#[embassy_executor::task]
async fn net_task(runner: &'static mut Runner<'static, WifiDevice<'static, WifiStaDevice>>) -> ! {
runner.run().await
println!("Web server started...");
}
```
60 changes: 32 additions & 28 deletions src/wifi/web-server/serve-website.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Serve Webpage
# Web Module - Serve Webpage

We have completed the boilerplate for the Wi-Fi connection. Next, we will use the picoserve crate to set up a route for the root URL ("/") that will serve our HTML page.

## `impl_trait_in_assoc_type` feature
The picoserve crate requires the use of the `impl_trait_in_assoc_type` feature, which is currently an unstable feature in Rust. To enable this feature, you need to add the following line to the top of your async_main.rs file:
The picoserve crate requires the use of the `impl_trait_in_assoc_type` feature, which is currently an unstable feature in Rust. To enable this feature, you need to add the following line to the top of your lib.rs file(which we did already):
```rust
#![feature(impl_trait_in_assoc_type)]
```
Expand All @@ -13,7 +13,7 @@ The picoserve crate requires the use of the `impl_trait_in_assoc_type` feature,
The picoserve crate provides various traits to configure routing and other features needed for a web application. The `AppBuilder` trait is used to create a static router without state, while the `AppWithStateBuilder` trait allows for a static router with application state. Since our application only serves a single HTML page and doesn't require state, we will implement the AppBuilder trait. You can find more examples of how to use picoserve [here](https://github.com/sammhicks/picoserve/tree/development/examples).

```rust
struct Application;
pub struct Application;

impl AppBuilder for Application {
type PathRouter = impl routing::PathRouter;
Expand All @@ -37,53 +37,57 @@ Then, we need to implement the build_app function, which returns a Router instan
We need to start multiple tasks to handle incoming requests. Soon, we'll create a web_task function(an Embassy task) with a pool size set by a constant value we define now. Then, we'll launch tasks in a loop based on this value, which controls how many tasks can run concurrently.

```rust
const WEB_TASK_POOL_SIZE: usize = 2;
pub const WEB_TASK_POOL_SIZE: usize = 2;
```

We have set the pool size to 2. In the picoserve's examples, the pool size is set to 8. You can increase the pool size, but keep in mind that you'll also need to adjust resources like sockets and memory arena accordingly.


## Launch the App
Note: This code should be placed inside the main function after setting up the Wi-Fi tasks and obtaining the IP address.
## Web Application

We will create an instance of the AppRouter by calling the build_app function. We need to make it static with the help of the make_static macro so that we can use it in multiple asynchronous tasks.
We will define a WebApp struct that contains an instance of the picoserve router and the configuration.

```rust
let app = picoserve::make_static!(AppRouter<Application>, Application.build_app());
pub struct WebApp {
pub router: &'static Router<<Application as AppBuilder>::PathRouter>,
pub config: &'static picoserve::Config<Duration>,
}
```

We will configure the server settings with timeouts to control how long to wait before timing out on different operations. These timeouts apply when starting to read a request, waiting to read, or waiting to write the response. If any of these operations take too long, the connection will be closed.

```rust
let config = picoserve::make_static!(
picoserve::Config<Duration>,
picoserve::Config::new(picoserve::Timeouts {
start_read_request: Some(Duration::from_secs(5)),
read_request: Some(Duration::from_secs(1)),
write: Some(Duration::from_secs(1)),
})
.keep_connection_alive()
);
```
Next, we implement the Default trait for WebApp and initialize the picoserve Router by calling build_app. We also configure server timeouts to control the duration for operations like reading a request, waiting to read, or waiting to write a response. If any operation exceeds the timeout, the connection will be closed.

This is straightforward logic. We launch multiple tasks to handle incoming requests based on the pool size we set earlier. For each task, we pass the task id (iteration number), app instance, network stack instance, and server settings.

```rust
for id in 0..WEB_TASK_POOL_SIZE {
spawner.must_spawn(web_task(id, *stack, app, config));
impl Default for WebApp {
fn default() -> Self {
let router = picoserve::make_static!(AppRouter<Application>, Application.build_app());

let config = picoserve::make_static!(
picoserve::Config<Duration>,
picoserve::Config::new(picoserve::Timeouts {
start_read_request: Some(Duration::from_secs(5)),
read_request: Some(Duration::from_secs(1)),
write: Some(Duration::from_secs(1)),
})
.keep_connection_alive()
);

Self { router, config }
}
}
```

## Web Task function

We have created an Embassy task and specified the pool size in the attribute. The web server will listen on port 80. For each task, we define the TCP read and write buffers, along with the HTTP buffer. Finally, we call the listen_and_serve function from picoserve to handle incoming requests.

```rust

#[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)]
async fn web_task(
pub async fn web_task(
id: usize,
stack: Stack<'static>,
app: &'static AppRouter<Application>,
router: &'static AppRouter<Application>,
config: &'static picoserve::Config<Duration>,
) -> ! {
let port = 80;
Expand All @@ -93,7 +97,7 @@ async fn web_task(

picoserve::listen_and_serve(
id,
app,
router,
config,
stack,
port,
Expand Down
Loading

0 comments on commit cae6a77

Please sign in to comment.