-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Turn on led via Wi-Fi and webserver on ESP32
- Loading branch information
1 parent
d69dac1
commit d5ea7f3
Showing
6 changed files
with
275 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
))); | ||
``` | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters