weather station

This commit is contained in:
ImplFerris 2025-04-19 09:14:50 +05:30
parent a7711816dd
commit d315953ec4
9 changed files with 993 additions and 0 deletions

View File

@ -131,4 +131,10 @@
- [Circuit](./e-ink/circuit.md)
- [Draw Text](./e-ink/draw-text.md)
- [Draw Image](./e-ink/draw-image.md)
- [Weather Station](./e-ink/weather-station/index.md)
- [icons](./e-ink/weather-station/icons.md)
- [Wi-Fi](./e-ink/weather-station/wifi.md)
- [API](./e-ink/weather-station/weather-api.md)
- [Dashboard](./e-ink/weather-station/dashboard.md)
- [Fun](./e-ink/weather-station/main.md)
- [Projects](./projects.md)

View File

@ -42,6 +42,18 @@ tinybmp = "0.6.0"
We've already covered the details of the other crates, as well as the basic setup for the SPI and display module code. So, we won't go over those details again. Instead, let's jump straight into displaying an image and a shape after clearing the screen.
## Black background
Instead of using a white background, we'll fill the background with black for this project. I chose black because the image we're working with has a dark background, while the main subject is composed of white color. This will give better look.
```rust
// Clear any existing image
epd.clear_frame(&mut spi_dev, &mut Delay).unwrap();
display.clear(Color::Black).unwrap();
epd.update_and_display_frame(&mut spi_dev, display.buffer(), &mut Delay)
.unwrap();
Timer::after(Duration::from_secs(5)).await;
```
## Display the image
Place the ferris.bmp file inside the src folder. The code is pretty straightforward: load the image as bytes and pass it to the from_slice function of the Bmp. Then, you can use it with the Image.

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,294 @@
# Dashboard
In this chapter, we'll build the main logic. The idea is to update the dashboard every so often;say, every 10 minutes. To do that, we'll fetch the latest weather data by calling the access_website function from the weather module, and then update each part of the dashboard with the new info.
## Type aliases
We'll create type aliases for the SPI device and the e-paper display to make the code easier to read and work with.
```rust
type SpiDevice = ExclusiveDevice<Spi<'static, esp_hal::Blocking>, Output<'static>, Delay>;
type EPD = Epd1in54<SpiDevice, Input<'static>, Output<'static>, Output<'static>, Delay>;
```
## Dashboard struct
We'll define a Dashboard struct that takes the Wi-Fi stack, e-paper driver, and SPI device as inputs. The new function will initialize the struct and set up a default e-paper display.
```rust
pub struct Dashboard {
display: Display1in54,
wifi: Stack<'static>,
epd: EPD,
spi_dev: SpiDevice,
}
impl Dashboard {
pub fn new(wifi: Stack<'static>, epd: EPD, spi_dev: SpiDevice) -> Self {
Self {
display: Display1in54::default(),
wifi,
epd,
spi_dev,
}
}
}
```
Note: The following functions which take self as the first parameter should also be included inside the impl block.
## Dashboard Startup
This will be the starting function, which we'll call next in the main.rs file. It will accept SHA and RSA peripherals as input, which are needed to set up TLS. We will also instantiate the WeatherApi.
I rotated the display 90 degrees. Technically, you don't need to do this since the display we're using is 200x200 pixels. But if you're using a rectangular display, it makes more sense to rotate it.
In a loop, every 10 minutes, we'll call the refresh function which will fetch the latest data and update the display.
```rust
pub async fn start(&mut self, sha: SHA, rsa: RSA) {
self.display.set_rotation(DisplayRotation::Rotate90);
let tls = Tls::new(sha)
.expect("TLS::new with peripherals.SHA failed")
.with_hardware_rsa(rsa);
let api = WeatherApi::new(self.wifi);
loop {
self.refresh(&api, tls.reference()).await;
Timer::after(Duration::from_secs(60 * 10)).await;
}
}
```
## Refreshing the Display with Latest Weather Data
First, we get the latest weather data by calling the `access_website` function from `WeatherApi`. Then, we wake up the e-paper display, since we will be putting it to sleep at the end. After that, we clear the previous frame and fill the display with white.
Next, we draw the updated weather info - first the date, then the weather icon and temperature. After that, we add humidity, wind speed, and finally the signature at the bottom (just a small "implRust" text). Once everything is drawn, we update the display with the new frame, wait for 5 seconds, and then put it to sleep.
```rust
pub async fn refresh(&mut self, api: &WeatherApi, tls_reference: TlsReference<'_>) {
info!("Getting weather data");
let weather_data = api.access_website(tls_reference).await;
info!("Got weather data");
self.epd.wake_up(&mut self.spi_dev, &mut Delay).unwrap();
Timer::after(Duration::from_secs(5)).await;
// Clear any existing image
self.epd.clear_frame(&mut self.spi_dev, &mut Delay).unwrap();
self.display.clear(Color::White).unwrap();
self.epd
.update_and_display_frame(&mut self.spi_dev, self.display.buffer(), &mut Delay)
.unwrap();
Timer::after(Duration::from_secs(5)).await;
self.draw_date(weather_data.dt);
self.draw_icon(weather_data.weather[0].id.icon(), Point::new(20, 50));
self.draw_temperature(weather_data.main.temp, Point::new(20 + 70, 60));
self.draw_humidity(weather_data.main.humidity);
self.draw_wind(weather_data.wind.speed);
self.draw_signature();
self.epd
.update_and_display_frame(&mut self.spi_dev, self.display.buffer(), &mut Delay)
.unwrap();
Timer::after(Duration::from_secs(5)).await;
self.epd.sleep(&mut self.spi_dev, &mut Delay).unwrap();
}
```
## Get Icon helper function
This simple helper function takes the icon's name and the position where it should be drawn. It maps the icon name to the corresponding image bytes, then uses tinybmp and embedded_graphics to convert the bytes into an image, which is then rendered on the display.
```rust
fn draw_icon(&mut self, icon_name: &'static str, pos: Point) {
let img_bytes = self.get_icon(icon_name).unwrap();
let bmp = Bmp::from_slice(img_bytes).unwrap();
let image = Image::new(&bmp, pos);
image.draw(&mut self.display).unwrap();
}
pub fn get_icon(&self, icon_name: &'static str) -> Option<&'static [u8]> {
ICONS
.iter()
.find(|(name, _)| *name == icon_name)
.map(|(_, img_bytes)| *img_bytes)
}
```
## Display Temperature
We will display the temperature on the screen, format it with a "°C" suffix, and draw a horizontal line below it.
```rust
fn draw_temperature(&mut self, temperature: f64, pos: Point) {
let text_style = MonoTextStyle::new(&PROFONT_24_POINT, Color::Black);
info!("Drawing temperature");
let mut text: String<20> = String::new();
write!(&mut text, "{}°C", temperature).unwrap();
Text::with_baseline(&text, pos, text_style, Baseline::Top)
.draw(&mut self.display)
.unwrap();
Line::new(Point::new(0, 105), Point::new(200, 105))
.into_styled(PrimitiveStyle::with_stroke(Color::Black, 5))
.draw(&mut self.display)
.unwrap();
}
```
## Display Humidity
We will show the humidity icon on the screen, then display the humidity value next to it, followed by a vertical line for separation.
```rust
fn draw_humidity(&mut self, humidity: i32) {
self.draw_icon("humidity_percentage.bmp", Point::new(5, 110));
let text_style = MonoTextStyle::new(&PROFONT_18_POINT, Color::Black);
let mut text: String<10> = String::new();
write!(&mut text, "{}", humidity).unwrap();
Text::with_baseline(&text, Point::new(5 + 50, 120), text_style, Baseline::Top)
.draw(&mut self.display)
.unwrap();
Line::new(Point::new(5 + 85, 120), Point::new(5 + 85, 120 + 30))
.into_styled(PrimitiveStyle::with_stroke(Color::Black, 5))
.draw(&mut self.display)
.unwrap();
}
```
## Draw Wind
We will display the wind speed on the screen by first drawing the wind icon. Then, we will show the wind speed value followed by the unit "m/s" on the display.
```rust
fn draw_wind(&mut self, wind_speed: f64) {
self.draw_icon("air.bmp", Point::new(100, 110));
let text_style = MonoTextStyle::new(&PROFONT_18_POINT, Color::Black);
let mut text: String<10> = String::new();
write!(&mut text, "{}", wind_speed).unwrap();
Text::with_baseline(&text, Point::new(100 + 50, 120), text_style, Baseline::Top)
.draw(&mut self.display)
.unwrap();
let text_style = MonoTextStyleBuilder::new()
.font(&FONT_10X20)
.text_color(Color::Black)
.build();
Text::with_baseline("m/s", Point::new(100 + 50, 140), text_style, Baseline::Top)
.draw(&mut self.display)
.unwrap();
}
```
## Display Date
We will display the current date on the screen by formatting it with the day, month, and year. Then, we will render the text at the specified position and draw a horizontal line below it for separation.
```rust
fn draw_date(&mut self, dt: DateTime<Utc>) {
let text_style = MonoTextStyle::new(&PROFONT_24_POINT, Color::Black);
let mut text: String<24> = String::new();
write!(
&mut text,
"{} {} {}",
dt.day(),
month_name(dt.month()),
dt.year()
)
.unwrap();
Text::with_baseline(&text, Point::new(20, 10), text_style, Baseline::Top)
.draw(&mut self.display)
.unwrap();
Line::new(Point::new(0, 45), Point::new(200, 45))
.into_styled(PrimitiveStyle::with_stroke(Color::Black, 5))
.draw(&mut self.display)
.unwrap();
}
```
## Display Signature
This isn't related to the weather; we're just displaying "implRust" for fun. We calculate the center position, draw a black rectangle in the center, and place the text right in the middle.
```rust
fn draw_signature(&mut self) {
let display_width = epd1in54_v2::WIDTH as i32;
let rect_padding = 20;
let rect_width = display_width - 2 * rect_padding;
let rect_height = 40;
let rect_x = rect_padding;
let rect_y = 170;
let style = PrimitiveStyleBuilder::new()
.stroke_color(Color::Black)
.stroke_width(3)
.fill_color(Color::Black)
.build();
Rectangle::new(
Point::new(rect_x, rect_y),
Size::new(rect_width as u32, rect_height as u32),
)
.into_styled(style)
.draw(&mut self.display)
.unwrap();
let text = "implRust";
let text_style = MonoTextStyle::new(&PROFONT_24_POINT, Color::White);
let char_width = PROFONT_24_POINT.character_size.width as i32;
let text_width = text.len() as i32 * char_width;
let text_x = rect_x + (rect_width - text_width) / 2;
Text::with_baseline(
text,
Point::new(text_x as i32, rect_y),
text_style,
Baseline::Top,
)
.draw(&mut self.display)
.unwrap();
}
```
## Month Name Helper Function
We have created a helper function that returns the abbreviated month name based on the month number.
```rust
fn month_name(month: u32) -> &'static str {
match month {
1 => "Jan",
2 => "Feb",
3 => "Mar",
4 => "Apr",
5 => "May",
6 => "Jun",
7 => "Jul",
8 => "Aug",
9 => "Sep",
10 => "Oct",
11 => "Nov",
12 => "Dec",
_ => "Err",
}
}
```

View File

@ -0,0 +1,83 @@
## Icons
We'll need a set of icons to indicate weather, humidity, air quality, and other information. I've converted these icons from Google Fonts into BMP files, which we can use with the tinybmp crate.
We'll store the image bytes in a static array, allowing us to iterate through them and find the icon that matches the current weather. To achieve this, I added a new function in `build.rs` that loads icon files from the specified directory and generates a static array in `icon.rs`. This way, we don't need to manually update the array each time a new icon is added.
## Modify build.rs file
In build.rs, we'll add a function that scans the `src/icons/` directory. You can grab the icons from this repo: https://github.com/ImplFerris/esp32-epaper-weather/tree/main/src/icons and place them into your own `src/icons` folder.
```rust
fn generate_icon_array() {
let dest_path = Path::new("src/icons.rs");
let icon_dir = Path::new("src/icons/");
let mut generated_code = String::new();
generated_code.push_str("pub static ICONS: &[(&str, &[u8])] = &[\n");
if let Ok(entries) = fs::read_dir(icon_dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
generated_code.push_str(&format!(
" (\"{}\", include_bytes!(\"{}/{}\")),\n",
file_name, "icons", file_name
));
}
}
}
generated_code.push_str("];\n");
fs::write(dest_path, generated_code).expect("Failed to write icons.rs");
println!("cargo:rerun-if-changed={}", icon_dir.to_str().unwrap());
}
```
Update the main function in build.rs to call our new function
```rust
fn main() {
generate_icon_array();
linker_be_nice();
println!("cargo:rustc-link-arg=-Tdefmt.x");
// make sure linkall.x is the last linker script (otherwise might cause problems with flip-link)
println!("cargo:rustc-link-arg=-Tlinkall.x");
}
```
## icons.rs - Don't modify this manually!
If everything went smoothly, you should see the icons.rs file in the src folder with the content below. If the file wasn't created, try triggering the build script manually by running `cargo build`
```rust
pub static ICONS: &[(&str, &[u8])] = &[
("partly_cloudy_day.bmp", include_bytes!("icons/partly_cloudy_day.bmp")),
("nights_stay.bmp", include_bytes!("icons/nights_stay.bmp")),
("humidity_percentage.bmp", include_bytes!("icons/humidity_percentage.bmp")),
("cyclone.bmp", include_bytes!("icons/cyclone.bmp")),
("foggy.bmp", include_bytes!("icons/foggy.bmp")),
("snowing_heavy.bmp", include_bytes!("icons/snowing_heavy.bmp")),
("sunny.bmp", include_bytes!("icons/sunny.bmp")),
("cloud.bmp", include_bytes!("icons/cloud.bmp")),
("partly_cloudy_night.bmp", include_bytes!("icons/partly_cloudy_night.bmp")),
("rainy.bmp", include_bytes!("icons/rainy.bmp")),
("heat.bmp", include_bytes!("icons/heat.bmp")),
("clear_day.bmp", include_bytes!("icons/clear_day.bmp")),
("weather_mix.bmp", include_bytes!("icons/weather_mix.bmp")),
("weather_hail.bmp", include_bytes!("icons/weather_hail.bmp")),
("rainy_snow.bmp", include_bytes!("icons/rainy_snow.bmp")),
("mist.bmp", include_bytes!("icons/mist.bmp")),
("snowing.bmp", include_bytes!("icons/snowing.bmp")),
("rainy_heavy.bmp", include_bytes!("icons/rainy_heavy.bmp")),
("flood.bmp", include_bytes!("icons/flood.bmp")),
("sunny_snowing.bmp", include_bytes!("icons/sunny_snowing.bmp")),
("air.bmp", include_bytes!("icons/air.bmp")),
("storm.bmp", include_bytes!("icons/storm.bmp")),
("weather_snowy.bmp", include_bytes!("icons/weather_snowy.bmp")),
("thunderstorm.bmp", include_bytes!("icons/thunderstorm.bmp")),
("thermostat.bmp", include_bytes!("icons/thermostat.bmp")),
("rainy_light.bmp", include_bytes!("icons/rainy_light.bmp")),
];
```

View File

@ -0,0 +1,117 @@
# Write Rust code to build a Weather Dashboard using E-Paper and ESP32
Let's do something more fun with the e-ink display than just showing static text or images. We'll build a simple weather station that fetches real-time weather data from the internet using an HTTP API and updates the display with the latest info. This is like a "Hello, World" program for the e-paper.
## Prerequisite
We'll be using the API from "openweathermap.org" to fetch the weather data. The website gives you a free API key with a limit of 1000 requests per day, which should be enough. Go to their website, sign up for a free account, and you'll get your own API key.
### Generate project using esp-generate
To create the project, use the `esp-generate` command. Run the following:
```sh
esp-generate --chip esp32 weather-station
```
This will open a screen asking you to select options.
- First, select the option "Enable unstable HAL features."
- Select the option "Enable allocations via the esp-alloc crate."
- Now, you can enable "Enable Wi-Fi via esp-wifi crate."
Just save it by pressing "s" in the keyboard.
## Dependencies
Update your Cargo.toml to increase the task arena size for Embassy, and include the additional dependencies
```toml
embassy-executor = { version = "0.7.0", features = [
"defmt",
"task-arena-size-32768",
] }
```
**Additional dependencies:**
Let me give you a quick overview of the new dependencies. We've used `embedded-graphics` and `tinybmp` plenty of times already, including in the last chapter, so you should be pretty familiar with them by now. We will use them to draw shapes and load bmp files.
```toml
embedded-graphics = "0.8.1"
tinybmp = "0.6.0"
profont = "0.7.0"
chrono = { version = "0.4.40", default-features = false, features = ["serde"] }
serde = { version = "1.0.219", default-features = false, features = ["derive"] }
serde-json-core = "0.6.0"
serde_repr = "0.1.20"
reqwless = { default-features = false, features = [
"esp-mbedtls",
"log",
], git = "https://github.com/ImplFerris/reqwless", branch = "esp-hal-1.0.0" }
esp-mbedtls = { git = "https://github.com/esp-rs/esp-mbedtls.git", rev = "03458c3", features = [
"esp32",
] }
epd-waveshare = { features = [
"graphics",
], git = "https://github.com/ImplFerris/epd-waveshare"}
```
- [profont](https://docs.rs/profont/latest/profont/): The crate provides ProFont monospace programming font for embedded-graphics. We're using it because we need a slightly larger font size for the weather dashboard.
- [chrono](https://docs.rs/chrono/latest/chrono/): Used for handling date and time. We'll use it to deserialize the current datetime received from the API.
- [serde](https://docs.rs/serde/latest/serde/): A framework for serializing and deserializing Rust data structures.
- [serde_json_core](https://docs.rs/serde-json-core/latest/serde_json_core/) : To deserialize JSON into a Rust struct, we'll use this crate. It's designed specifically for no_std environments—normally, you'd use serde_json instead.
- [serde_repr](https://docs.rs/serde_repr/latest/serde_repr/): The crate allows you to serialize and deserialize enums using their numeric values, making it easy to map numeric weather condition codes (like 200 or 201) from the API to Rust enum variants.
- [reqwless](https://docs.rs/reqwless/latest/reqwless/index.html): We've already used this crate, which is an HTTP client that works in a no_std environment. We'll use it to send requests and receive responses from the API. (Note: We're using a forked version of reqwless to make it work with esp-hal 1.0.0, since the original crate isn't compatible at the moment.)
- [esp-mbedtls](https://github.com/esp-rs/esp-mbedtls) : The reqwless crate supports two TLS backends: embedded-tls (the default) and esp-mbedtls. By default, reqwless uses embedded-tls, which supports only TLS 1.3. However, OpenWeatherMap doesnt support TLS 1.3 at the moment. So, to make it work, we need to use the esp-mbedtls crate instead.
- [epd-waveshare](https://docs.rs/epd-waveshare/latest/epd_waveshare/) : A simple driver for Waveshare E-Ink displays over SPI. The original crate didn't work properly with the 1.54-inch variant, so I had to fork it and patch the code. It's more of a hack than a proper fix, so I haven't sent a PR to the original repo yet. For now, we will use the forked version.
## Project structure
This is the overall project structure, and we'll walk through it step by step.
```
├── build.rs
├── src
│ ├── bin
│ │ └── main.rs
│ ├── ca_cert.pem
│ ├── dashboard.rs
│ ├── icons
│ │ ├── air.bmp
│ │ ├── ...bmp
│ │ └── ...bmp
│ ├── icons.rs
│ ├── lib.rs
│ ├── weather.rs
│ └── wifi.rs
├── ...
```
Since this will be a big chapter, I recommend referring to the finished project here (https://github.com/ImplFerris/esp32-epaper-weather/) for reference. You might need to check it for imports I won't cover them in the tutorial and other non-important details.
## The lib module
In `lib.rs`, we define the submodules along with the helper macro we've been using in the Wi-Fi section. This macro reserves memory at compile time for a `static` value, but allows it to be initialized at runtime.
```rust
#![no_std]
pub mod dashboard;
pub mod icons;
pub mod weather;
pub mod wifi;
#[macro_export]
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
}
```

View File

@ -0,0 +1,81 @@
# Let the fun begin
In the `main` function, we will perform the usual setup steps like initializing the Wi-Fi stack, SPI device, and creating an instance of the `Dashboard` we defined earlier.
One important thing to note is that we allocate memory for the `dram2` using `esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);`. This is crucial because the mbedtls requires additional heap memory. Without this allocation, you will encounter memory allocation failures when sending the API request.
```rust
#[esp_hal_embassy::main]
async fn main(spawner: Spawner) {
// generator version: 0.3.1
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
esp_alloc::heap_allocator!(size: 80 * 1024);
esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);
let timer0 = TimerGroup::new(peripherals.TIMG1);
esp_hal_embassy::init(timer0.timer0);
info!("Embassy initialized!");
let timer1 = TimerGroup::new(peripherals.TIMG0);
let rng = Rng::new(peripherals.RNG);
let esp_wifi_ctrl = &*lib::mk_static!(
EspWifiController<'static>,
esp_wifi::init(timer1.timer0, rng.clone(), peripherals.RADIO_CLK,).unwrap()
);
// Configure and Start Wi-Fi tasks
let stack = lib::wifi::start_wifi(esp_wifi_ctrl, peripherals.WIFI, rng, &spawner).await;
// Initialize SPI
let spi = Spi::new(
peripherals.SPI2,
SpiConfig::default()
.with_frequency(Rate::from_mhz(4))
.with_mode(SpiMode::_0),
)
.unwrap()
//CLK
.with_sck(peripherals.GPIO18)
//DIN
.with_mosi(peripherals.GPIO23);
let cs = Output::new(peripherals.GPIO33, Level::Low, OutputConfig::default());
let mut spi_dev = ExclusiveDevice::new(spi, cs, Delay);
// Initialize Display
let busy_in = Input::new(
peripherals.GPIO22,
InputConfig::default().with_pull(Pull::None),
);
let dc = Output::new(peripherals.GPIO17, Level::Low, OutputConfig::default());
let reset = Output::new(peripherals.GPIO16, Level::Low, OutputConfig::default());
let epd = Epd1in54::new(&mut spi_dev, busy_in, dc, reset, &mut Delay, None).unwrap();
let mut app = Dashboard::new(stack, epd, spi_dev);
app.start(peripherals.SHA, peripherals.RSA).await;
}
```
## Clone the existing project
You can also clone (or refer) project I created and navigate to the `wifi-webfetch` folder.
```sh
git clone https://github.com/ImplFerris/esp32-epaper-weather/
cd esp32-epaper-weather
```
### How to run?
We will need to pass the Wi-Fi name (SSID), Wi-Fi password, and Open Weather API key as environment variables when flashing the program onto the ESP32.
```sh
SSID=YOUR_WIFI_NAME PASSWORD=YOUR_WIFI_PASSWORD API_KEY=OPEN_WEATHER_KEY cargo run --release
```
If everything goes successfully, the e-paper display will flicker briefly to clear and render the content, and you should see the weather data displayed.
<img style="display: block; margin: auto;" src="../images/e-paper-weather-station.jpg"/>

View File

@ -0,0 +1,291 @@
# Weather API
In this chapter, we'll learn how to send a request to the API and deserialize the JSON response into a struct.
I hope you have obtained API key for the openweathermap website. We can pass it through environment variable. So we define constant like this
```rust
const API_KEY: &str = env!("API_KEY");
```
## JSON to Struct
I want you to go through the API documentation at "https://openweathermap.org/current" and familiarize yourself with the JSON response structure. At the top level, I defined a WeatherData struct with fields like weather, main, and wind, each using custom struct types. To avoid cluttering the tutorial, I'll include the definitions of other structs at the end of the chapter.
```rust
#[derive(Debug, Deserialize)]
pub struct WeatherData {
pub weather: Vec<Weather, 4>,
pub main: Main,
pub wind: Wind,
#[serde(with = "chrono::serde::ts_seconds")]
pub dt: DateTime<Utc>,
pub name: String<20>,
}
```
Here, I'm only deserializing the fields we need and ignoring the rest of the JSON data.
## TLS Certificate
The OpenWeatherMap website does not support TLS 1.3. If it did, this task would have been much simpler;we could have just used the embedded-tls crate, which supports only TLS 1.3. embedded-tls also provides a TLSVerify::None option, allowing us to skip SSL certificate verification. Unfortunately, since we're limited to TLS 1.2, we have to use esp-mbedtls, which requires certificate verification.
No worries--this is a great opportunity to introduce an alternative approach as well.
In contrast to standard environments, where the operating system provides a built-in set of trusted certificate chains (and the browser or HTTP client handles verification automatically), our embedded environment lacks this support. Therefore, we must manually obtain the TLS certificate from the website and store it in a file, so our program can use it for verification.
You can run this command and store the file in the `src` folder.
```sh
openssl s_client -showcerts -connect openweathermap.org:443 </dev/null 2>/dev/null | \
awk '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/' > ca_cert.pem
```
Alternatively, you can download it from my project repository [here](https://github.com/ImplFerris/esp32-epaper-weather/blob/main/src/ca_cert.pem), though this isn't recommended as the file may be outdated.
## Weather API
Let's create a wrapper struct to handle API requests. While a simple function could work, it would require passing the URL and Wi-Fi stack every time. By using a struct, we can instantiate it once and call the access_website method whenever we need the latest weather data.
```rust
pub struct WeatherApi {
wifi: Stack<'static>,
url: String<120>,
}
```
In the new function, we'll construct the API URL by appending the API key and store it in the field.
NOTE: If you observed, I have used "London" as the city name. You can replace it with your city name along with the country code. Refer to the doc for more details and adjust accordingly.
```rust
pub fn new(wifi: Stack<'static>) -> Self {
let mut url = String::new();
url.push_str(
"https://api.openweathermap.org/data/2.5/weather?q=London&units=metric&appid=",
)
.unwrap();
url.push_str(API_KEY).unwrap();
Self { wifi, url }
}
```
## Sending API Request
Next, we'll instantiate the DNS socket and TCP client. When configuring TLS, we'll specify the TLS 1.2 protocol and set up the certificate using the CA chain. The certificate content will be loaded from the "ca_cert.pem" file we created earlier.
With the basic setup in place, we'll pass the TCP client, DNS socket, and TLS configuration to the HttpClient, and use it to send a request to the API URL. Once we receive the response, we'll deserialize the JSON payload into a WeatherData struct using serde_json_core.
```rust
pub async fn access_website(&self, tls_reference: TlsReference<'_>) -> WeatherData {
let dns = DnsSocket::new(self.wifi);
let tcp_state = TcpClientState::<1, 4096, 4096>::new();
let tcp = TcpClient::new(self.wifi, &tcp_state);
let tls_config = TlsConfig::new(
reqwless::TlsVersion::Tls1_2,
reqwless::Certificates {
ca_chain: reqwless::X509::pem(
concat!(include_str!("./ca_cert.pem"), "\0").as_bytes(),
)
.ok(),
..Default::default()
},
tls_reference,
);
let mut client = HttpClient::new_with_tls(&tcp, &dns, tls_config);
let mut buffer = [0u8; 4096];
let mut http_req = client
.request(reqwless::request::Method::GET, &self.url)
.await
.unwrap();
let response = http_req.send(&mut buffer).await.unwrap();
info!("Got response");
let res = response.body().read_to_end().await.unwrap();
let (data, _): (WeatherData, _) = serde_json_core::de::from_slice(res).unwrap();
data
}
```
## The rest of the struct
One thing to note here: the API gives the weather condition as a number (it also provides it as a string, but the number is easier to work with). We'll map those numbers to our ConditionCode enum. We've also added an icon function to that enum, which tells us which icon to show for each specific weather condition.
```rust
#[derive(Debug, Deserialize_repr)]
#[repr(u16)]
pub enum ConditionCode {
// Group 2xx: Thunderstorm
ThunderstormWithLightRain = 200,
ThunderstormWithRain = 201,
ThunderstormWithHeavyRain = 202,
LightThunderstorm = 210,
Thunderstorm = 211,
HeavyThunderstorm = 212,
RaggedThunderstorm = 221,
ThunderstormWithLightDrizzle = 230,
ThunderstormWithDrizzle = 231,
ThunderstormWithHeavyDrizzle = 232,
// Group 3xx: Drizzle
LightIntensityDrizzle = 300,
Drizzle = 301,
HeavyIntensityDrizzle = 302,
LightIntensityDrizzleRain = 310,
DrizzleRain = 311,
HeavyIntensityDrizzleRain = 312,
ShowerRainAndDrizzle = 313,
HeavyShowerRainAndDrizzle = 314,
ShowerDrizzle = 321,
// Group 5xx: Rain
LightRain = 500,
ModerateRain = 501,
HeavyIntensityRain = 502,
VeryHeavyRain = 503,
ExtremeRain = 504,
FreezingRain = 511,
LightIntensityShowerRain = 520,
ShowerRain = 521,
HeavyIntensityShowerRain = 522,
RaggedShowerRain = 531,
// Group 6xx: Snow
LightSnow = 600,
Snow = 601,
HeavySnow = 602,
Sleet = 611,
LightShowerSleet = 612,
ShowerSleet = 613,
LightRainAndSnow = 615,
RainAndSnow = 616,
LightShowerSnow = 620,
ShowerSnow = 621,
HeavyShowerSnow = 622,
// Group 7xx: Atmosphere
Mist = 701,
Smoke = 711,
Haze = 721,
SandDustWhirls = 731,
Fog = 741,
Sand = 751,
Dust = 761,
VolcanicAsh = 762,
Squalls = 771,
Tornado = 781,
// Group 800: Clear
ClearSky = 800,
// Group 80x: Clouds
FewClouds = 801,
ScatteredClouds = 802,
BrokenClouds = 803,
OvercastClouds = 804,
}
impl ConditionCode {
pub fn icon(&self) -> &'static str {
match self {
// Thunderstorm
ConditionCode::ThunderstormWithLightRain
| ConditionCode::ThunderstormWithRain
| ConditionCode::ThunderstormWithHeavyRain
| ConditionCode::LightThunderstorm
| ConditionCode::Thunderstorm
| ConditionCode::HeavyThunderstorm
| ConditionCode::RaggedThunderstorm
| ConditionCode::ThunderstormWithLightDrizzle
| ConditionCode::ThunderstormWithDrizzle
| ConditionCode::ThunderstormWithHeavyDrizzle => "storm.bmp",
// Drizzle
ConditionCode::LightIntensityDrizzle
| ConditionCode::Drizzle
| ConditionCode::HeavyIntensityDrizzle
| ConditionCode::LightIntensityDrizzleRain
| ConditionCode::DrizzleRain
| ConditionCode::HeavyIntensityDrizzleRain
| ConditionCode::ShowerRainAndDrizzle
| ConditionCode::HeavyShowerRainAndDrizzle
| ConditionCode::ShowerDrizzle => "rainy.bmp",
// Rain
ConditionCode::LightRain
| ConditionCode::ModerateRain
| ConditionCode::HeavyIntensityRain
| ConditionCode::VeryHeavyRain
| ConditionCode::ExtremeRain
| ConditionCode::LightIntensityShowerRain
| ConditionCode::ShowerRain
| ConditionCode::HeavyIntensityShowerRain
| ConditionCode::RaggedShowerRain => "rainy_heavy.bmp",
ConditionCode::FreezingRain => "weather_mix.bmp",
// Snow
ConditionCode::LightSnow
| ConditionCode::Snow
| ConditionCode::HeavySnow
| ConditionCode::Sleet
| ConditionCode::LightShowerSleet
| ConditionCode::ShowerSleet
| ConditionCode::LightRainAndSnow
| ConditionCode::RainAndSnow
| ConditionCode::LightShowerSnow
| ConditionCode::ShowerSnow
| ConditionCode::HeavyShowerSnow => "snowing.bmp",
// Atmosphere
ConditionCode::Mist
| ConditionCode::Smoke
| ConditionCode::Haze
| ConditionCode::SandDustWhirls
| ConditionCode::Fog
| ConditionCode::Sand
| ConditionCode::Dust
| ConditionCode::VolcanicAsh
| ConditionCode::Squalls => "foggy.bmp",
ConditionCode::Tornado => "cyclone.bmp",
// Clear
ConditionCode::ClearSky => "sunny.bmp",
// Clouds
ConditionCode::FewClouds
| ConditionCode::ScatteredClouds
| ConditionCode::BrokenClouds
| ConditionCode::OvercastClouds => "partly_cloudy_day.bmp",
}
}
}
#[derive(Debug, Deserialize)]
pub struct Weather {
pub id: ConditionCode,
}
#[derive(Debug, Deserialize)]
pub struct Main {
pub temp: f64,
pub feels_like: f64,
pub temp_min: f64,
pub temp_max: f64,
pub pressure: i32,
pub humidity: i32,
pub sea_level: Option<i32>,
pub grnd_level: Option<i32>,
}
#[derive(Debug, Deserialize)]
pub struct Wind {
pub speed: f64,
pub deg: f64,
pub gust: Option<f64>,
}
```

View File

@ -0,0 +1,109 @@
# Wi-Fi module
The `wifi.rs` code is the same as in the ['Access Website' chapter](../../wifi/embassy/async-access-website.md) of the Wi-Fi section. I recommend referring to it for more details.
```rust
use embassy_executor::Spawner;
use embassy_net::{DhcpConfig, Runner, Stack, StackResources};
use embassy_time::{Duration, Timer};
use esp_hal::rng::Rng;
use esp_println as _;
use esp_println::println;
use esp_wifi::wifi::{self, WifiController, WifiDevice, WifiEvent, WifiState};
use esp_wifi::EspWifiController;
use crate::mk_static;
const SSID: &str = env!("SSID");
const PASSWORD: &str = env!("PASSWORD");
#[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 = wifi::Configuration::Client(wifi::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
}
}
}
}
#[embassy_executor::task]
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
runner.run().await
}
pub async fn start_wifi(
esp_wifi_ctrl: &'static EspWifiController<'static>,
wifi: esp_hal::peripherals::WIFI,
mut rng: Rng,
spawner: &Spawner,
) -> Stack<'static> {
let (controller, interfaces) = esp_wifi::wifi::new(&esp_wifi_ctrl, wifi).unwrap();
let wifi_interface = interfaces.sta;
let net_seed = rng.random() as u64 | ((rng.random() as u64) << 32);
let dhcp_config = DhcpConfig::default();
let net_config = embassy_net::Config::dhcpv4(dhcp_config);
// Init network stack
let (stack, runner) = embassy_net::new(
wifi_interface,
net_config,
mk_static!(StackResources<3>, StackResources::<3>::new()),
net_seed,
);
spawner.spawn(connection_task(controller)).ok();
spawner.spawn(net_task(runner)).ok();
wait_for_connection(stack).await;
stack
}
async fn wait_for_connection(stack: Stack<'_>) {
println!("Waiting for link to be up");
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;
}
}
```