Skip to content

Commit

Permalink
Turn on led via Wi-Fi and webserver on ESP32
Browse files Browse the repository at this point in the history
  • Loading branch information
ImplFerris committed Jan 10, 2025
1 parent d69dac1 commit d5ea7f3
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 1 deletion.
1 change: 0 additions & 1 deletion src/#

This file was deleted.

3 changes: 3 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@
- [Webserver on ESP32](./wifi/web-server/index.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)
- [Control LED](./wifi/led/index.md)
- [API and Webpage](./wifi/led/webpage-control-led.md)
- [Projects](./projects.md)
81 changes: 81 additions & 0 deletions src/wifi/led/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Write Rust Code to Control ESP32 LED via Wi-Fi

You can configure a web server on the ESP32 to receive instructions via web requests, allowing you to control connected devices or execute specific actions. For instance, you can use a browser on your computer or mobile phone to send commands to turn an LED on or off, adjust motor speed, or retrieve sensor data.

In this section, we will create a simple web page that allows us to turn an LED on or off.

## Project base
This time, we are not going to set up the project with esp-generate. Instead, we will copy the webserver-base project and work on top of that.

I recommend you to read these section before you proceed furhter; This will avoid unnecessary repetition of code and explanations.
- [Creating Web Server](../web-server/index.md)
- [Assigning Static IP](../static-ip.md)


```sh
git clone https://github.com/ImplFerris/esp32-projects
cp -r esp32-projects/webserver-base ~/YOUR_PROJECT_FOLDER/wifi-led
```

## Serde
Serde is a Rust crate used for serializing and deserializing data structures. We will use it to handle the JSON data exchanged between the backend and the frontend.

Update the Cargo.toml with the following:
```toml
serde = { version = "1.0.217", default-features = false, features = ["derive"] }
```

## LED Task

I placed these code in the "led.rs" module.

First, we'll create an Embassy task to toggle the onboard LED state based on the value stored in the LED_STATE variable, which will use the AtomicBool type. "Atomic types provide primitive shared-memory communication between threads, and are the building blocks of other concurrent types". To learn more about Atomic types, refer to the Rust standard library documentation on [atomics](https://doc.rust-lang.org/beta/core/sync/atomic/index.html) or the [Rust Atomics and Locks](https://marabos.nl/atomics/) book.

```rust
use core::sync::atomic::{AtomicBool, Ordering};

use embassy_time::{Duration, Timer};
use esp_hal::gpio::Output;

pub static LED_STATE: AtomicBool = AtomicBool::new(false);

#[embassy_executor::task]
pub async fn led_task(mut led: Output<'static>) {
loop {
if LED_STATE.load(Ordering::Relaxed) {
led.set_high();
} else {
led.set_low();
}
Timer::after(Duration::from_millis(50)).await;
}
}
```

In the led_task function, we take an LED pin as argument and continuously checks the value of the LED_STATE variable in a loop. We read the value using the load method with Ordering::Relaxed. If the value is true, we turn on the LED. Otherwise, we turn off the LED.

In the main function, we spawn the led_task to run it in the background. We will pass the GPIO 2 pin(If you want to use an external LED, replace it with the pin to which you connected the LED), which is the onboard LED, and we will set the initial state of the LED to Low.

```rust
// LED Task
spawner.must_spawn(lib::led::led_task(Output::new(
peripherals.GPIO2,
Level::Low,
)));
```
















119 changes: 119 additions & 0 deletions src/wifi/led/webpage-control-led.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Control ESP32 LED with Webpage

We will create a simple API endpoint that accepts a boolean input to control the LED's state. Along with this, an "index.html" page will be served, displaying two buttons: one to turn the LED on and another to turn it off.

When you press one of the buttons, a request will be sent to the "/led" endpoint with the following JSON payload:

- `{ "is_on": true }` to turn the LED on
- `{ "is_on": false }` to turn the LED off

Based on the value of the "is_on" field, the "LED_STATE" variable of the led module will be updated. The "led_task" will then turn the LED on or off accordingly.

## Routing

In the "build_app" function, we configure the web routes for the application. The root path ("/") will serve the "index.html" content, we have to place this file inside "src/" folder. The "/led" path will accept "POST" requests and be handled by the "led_handler".

```rust
pub struct Application;

impl AppBuilder for Application {
type PathRouter = impl routing::PathRouter;

fn build_app(self) -> picoserve::Router<Self::PathRouter> {
picoserve::Router::new()
.route(
"/",
routing::get_service(File::html(include_str!("index.html"))),
)
.route("/led", routing::post(led_handler))
}
}
```

## LED Handler

We will define two structs, one for handling the incoming input and one for sending the response. The LedRequest struct will derive Deserialize to parse the incoming JSON and provide it as a struct instance. The LedResponse struct will derive Serialize to convert the struct instance and send it as a JSON response.

```rust
#[derive(serde::Deserialize)]
struct LedRequest {
is_on: bool,
}

#[derive(serde::Serialize)]
struct LedResponse {
success: bool,
}
```

In the led_handler function, the LedRequest is extracted as a parameter. We can directly store the "is_on" value in the LED_STATE since both are boolean. Finally, the handler will return a JSON response with a LedResponse indicating success.

```rust
async fn led_handler(input: picoserve::extract::Json<LedRequest, 0>) -> impl IntoResponse {
crate::led::LED_STATE.store(input.0.is_on, Ordering::Relaxed);

picoserve::response::Json(LedResponse { success: true })
}
```


## WebPage content

You can download the index.html file from [here](https://github.com/ImplFerris/esp32-projects/blob/main/wifi-led/src/index.html) and place it in the "src/" folder, or create your own custom content to send JSON requests.

**NOTE:**

You need to update the URL "http://192.168.0.50/led" with your ESP32's IP address. I've hardcoded it here for simplicity; otherwise, we would need to use a placeholder and replace it dynamically or adopt a template-based approach.

```html
<div class="button-container">
<button class="btn-on" onclick="sendRequest(true)">Turn on LED</button>
<button class="btn-off" onclick="sendRequest(false)">Turn off LED</button>
</div>

<script>
function sendRequest(is_on) {
const url = 'http://192.168.0.50/led'; // Replace with STATIC IP of ESP32
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ is_on })
})
.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Network response was not ok');
})
.then(data => {
console.log('Success:', data);
//alert(LED turned ${action});
})
.catch(error => {
console.error('Error:', error);
alert('Failed to send the request');
});
}
</script>
```



## Clone the existing project
You can also clone (or refer) project I created and navigate to the `wifi-led` folder.

```sh
git clone https://github.com/ImplFerris/esp32-projects
cd esp32-projects/wifi-led
```

### How to run?

Pass the Wi-Fi name, password, static IP, and gateway IP address as environment variables, then flash the ESP32

```sh
SSID=YOUR_WIFI_NAME PASSWORD=YOUR_WIFI_PASSWORD STATIC_IP=ASSIGN_ESP32_IP/24 GATEWAY_IP=WIFI_GATEWAY_IP cargo run --release
```
66 changes: 66 additions & 0 deletions src/wifi/static-ip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@

# Static IP Address

In previous exercises, we have been relying on the DHCP server to assign an IP address to the ESP32. However, this can be unreliable as the IP address may change over time. In many cases, we want to assign a static IP address to ensure the ESP32 always has a fixed, predictable address. This makes it easier to access the device consistently without having to check or update its IP address every time it reconnects to the network.

Now, we will remove the DHCP-related code and modify the net_config variable to initialize with a static IP address instead.

First, let's define the constants that will load the static IP and gateway IP from environment variables. The IP address should be in CIDR format, which includes both the IP address and the subnet mask. We need to specify the IP address followed by a slash and the subnet mask. For example, if you want to assign the IP address 192.168.0.50 to your ESP32, you should write it as 192.168.0.50/24.


<div class="alert-box alert-box-info">
<span class="icon"><i class="fa fa-info"></i></span>
<div class="alert-content">
<b class="alert-title">Finding IP</b>
<p>You can't assign just any IP address. You need to find the IP range your Wi-Fi router is using. To do this, you can type `ip a` in the terminal and look for the IP address next to your Wi-Fi interface (typically starting with `wl`). For example, if your system's IP address is 192.168.0.103, you can assign an IP address starting from 192.168.0.2</p>
</div>
</div>


```rust
// IP Address/Subnet mask eg: STATIC_IP=192.168.0.50/24
const STATIC_IP: &str = env!("STATIC_IP");
const GATEWAY_IP: &str = env!("GATEWAY_IP");
```

You will also need to configure the gateway, although it's not required for this exercise since we won't be sending requests to the internet. However, it's good practice to configure it for future exercises.

The gateway address is often the first address in your Wi-Fi IP range. For instance, if your IP addresses range from 192.168.0.1 to 192.168.0.255, the gateway is likely to be 192.168.0.1. You can also use the command `ip route | grep default` in linux to find your gateway address.

```rust
//find the `let net_config` part and replace
let Ok(ip_addr) = Ipv4Cidr::from_str(STATIC_IP) else {
println!("Invalid STATIC_IP");
loop {}
};

let Ok(gateway) = Ipv4Addr::from_str(GATEWAY_IP) else {
println!("Invalid GATEWAY_IP");
loop {}
};

let net_config = embassy_net::Config::ipv4_static(StaticConfigV4 {
address: ip_addr,
gateway: Some(gateway),
dns_servers: Vec::new(),
});
// You dont need to change anything in `embassy_net::new` call.
```


## Project Base

I have reorganized the project by splitting it into modules like wifi and web to keep the main file clean. We will use this project as the base for the upcoming exercise, so I recommend taking a look at how it is organized.

```sh
git clone https://github.com/ImplFerris/esp32-projects
cd esp32-projects/webserver-base
```

### How to run?

Normally, we would simply run `cargo run --release`, but this time we also need to pass the environment variables for the Wi-Fi connection.

```sh
SSID=YOUR_WIFI_NAME PASSWORD=YOUR_WIFI_PASSWORD STATIC_IP=ASSIGN_ESP32_IP/24 GATEWAY_IP=WIFI_GATEWAY_IP cargo run --release
```
6 changes: 6 additions & 0 deletions src/wifi/web-server/serve-website.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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:
```rust
#![feature(impl_trait_in_assoc_type)]
```

## Application and Routing

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).
Expand Down

0 comments on commit d5ea7f3

Please sign in to comment.