Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Как подключить ESP32 к Wi-Fi и открыть сайт

В этом упражнении мы настроим ESP32 в режиме STA для подключения к вашей Wi-Fi-сети. Затем отправим запрос с ESP32 на веб-сервер и выведем ответ в системную консоль. Этот код основан на примерах esp-hal.

Создание проекта с помощью esp-generate

Чтобы создать проект, используйте команду esp-generate. Выполните:

esp-generate --chip esp32 wifi-webfetch

Откроется экран с вопросом о выборе опций. Чтобы включить Wi-Fi, сначала нужно включить функции «unstable» и «alloc». Как вы могли заметить, пока не выбраны эти две опции, пункт Wi-Fi недоступен. Поэтому выбирайте их по одной:

  • Сначала выберите опцию «Enable unstable HAL features».
  • Выберите опцию «Enable allocations via the esp-alloc crate».
  • Теперь можно включить «Enable Wi-Fi via the esp-radio crate».

Также включите логирование.

  • Прокрутите до пункта «Flashing, logging and debugging (espflash)» и нажмите Enter.
  • Затем выберите «Use defmt to print messages».

Сохраните настройки, нажав на клавиатуре «s».

Обновляем зависимости

Добавьте в файл Cargo.toml следующие зависимости:

blocking-network-stack = { git = "https://github.com/bjoernQ/blocking-network-stack.git", rev = "b3ecefc222d8806edd221f266999ca339c52d34e", default-features = false, features = [
  "dhcpv4",
  "tcp",
] }

heapless = { version = "0.9.1" }

blocking-network-stack — это крейт, который предоставляет синхронные (не асинхронные) сетевые примитивы для TCP/UDP-обмена.

Из-за конфликта версий я понизил embedded-io до версии 0.6.1 (вместо 0.7.1, которую сгенерировал esp-generate).

embedded-io = { version = "0.6.1" }

Крейт esp-alloc

«Простой аллокатор кучи no_std для процессоров RISC-V и Xtensa от Espressif. Поддерживает все доступные на данный момент устройства ESP32». Крейт esp-alloc предоставляет глобальный аллокатор для SoC ESP.

Инициализация периферии

Создадим вспомогательную функцию, которая инициализирует ESP HAL и возвращает экземпляр периферии. Также настроим кучу размером 72 КиБ с помощью макроса esp_alloc::heap_allocator!.

#![allow(unused)]
fn main() {
fn init_hardware() -> Peripherals {
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);
    esp_alloc::heap_allocator!(size: 72 * 1024);
    peripherals
}
}

Инициализация контроллера Wi-Fi

Программе нужно передать имя Wi-Fi и пароль. Вместо того чтобы жёстко прописывать эти учётные данные в коде, загрузим их из переменных окружения. При запуске программы передадим переменные окружения вместе с ней.

#![allow(unused)]
fn main() {
const SSID: &str = env!("SSID");
const PASSWORD: &str = env!("PASSWORD");
}

Сначала инициализируем TimerGroup и генератор случайных чисел (RNG), которые нужны для настройки контроллера Wi-Fi. RNG также используется для инициализации синхронного стека TCP/IP, поэтому используем clone, чтобы можно было переиспользовать его.

#![allow(unused)]
fn main() {
let timg0 = TimerGroup::new(peripherals.TIMG0);
let rng = Rng::new();
}

Инициализируем контроллер Wi-Fi, используя аппаратный таймер, RNG и периферию тактирования. Затем создаём экземпляр драйвера Wi-Fi, который управляет сетевыми подключениями и различными интерфейсами. Наконец, настраиваем устройство на работу в режиме станции (STA), чтобы оно могло подключаться к сетям Wi-Fi как клиент.

#![allow(unused)]
fn main() {
esp_rtos::start(timg0.timer0);
let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller");

let (mut wifi_controller, interfaces) =
    esp_radio::wifi::new(&radio_init, peripherals.WIFI, Default::default())
        .expect("Failed to initialize Wi-Fi controller");

let mut device = interfaces.sta;
}

Инициализация SocketSet

Создадим SocketSet с местом для трёх сокетов, чтобы управлять несколькими сокетами внутри стека, например DHCP и TCP.

#![allow(unused)]
fn main() {
let mut socket_set_entries: [SocketStorage; 3] = Default::default();
let mut socket_set = SocketSet::new(&mut socket_set_entries[..]);
}

DHCP-сокет

Создадим DHCP-сокет, чтобы запросить IP-адрес у DHCP-сервера. Можно задать исходящие опции, в том числе имя хоста, через DHCP Client Option 12 (подробнее о DHCP-опциях — здесь). Добавим DHCP-сокет в SocketSet, который создали ранее.

#![allow(unused)]
fn main() {
let mut dhcp_socket = smoltcp::socket::dhcpv4::Socket::new();
// здесь можно задать имя хоста (или добавить другие DHCP-опции)
dhcp_socket.set_outgoing_options(&[DhcpOption {
    kind: 12,
    data: b"implRust",
}]);
socket_set.add(dhcp_socket);
// socket_set.add(smoltcp::socket::dhcpv4::Socket::new());
}

Инициализация сетевого стека

Инициализируем Stack из крейта smoltcp, используя сетевой интерфейс и устройство (которые мы получили из Wi-Fi-интерфейса, созданного ранее), сокеты, замыкание для получения времени и случайное число.

#![allow(unused)]
fn main() {
let now = || Instant::now().duration_since_epoch().as_millis();
let mut stack = Stack::new(
    create_interface(&mut device),
    device,
    socket_set,
    now,
    rng.random(),
);
}

Шаблонные функции для настройки сетевого интерфейса стека TCP/IP smoltcp с Wi-Fi-устройством (взяты из официальных примеров esp-hal)

#![allow(unused)]
fn main() {
pub fn create_interface(device: &mut esp_wifi::wifi::WifiDevice) -> smoltcp::iface::Interface {
    // можно создать несколько экземпляров, но так как WifiDevice один, сделать с ним что-то плохое вряд ли получится
    // скорее всего, с ним нельзя сделать ничего плохого
    smoltcp::iface::Interface::new(
        smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ethernet(
            smoltcp::wire::EthernetAddress::from_bytes(&device.mac_address()),
        )),
        device,
        timestamp(),
    )
}

// шаблонный код smoltcp
fn timestamp() -> smoltcp::time::Instant {
    smoltcp::time::Instant::from_micros(
        esp_hal::time::Instant::now()
            .duration_since_epoch()
            .as_micros() as i64,
    )
}
}

Режим работы Wi-Fi:

Далее настраиваем режим работы Wi-Fi с помощью перечисления ModeConfig. Поскольку ESP32 должен работать как Wi-Fi-клиент, используем вариант Client. Нужно указать SSID (идентификатор сети, то есть имя вашей сети Wi-Fi) и пароль Wi-Fi.

#![allow(unused)]
fn main() {
fn configure_wifi(controller: &mut WifiController<'_>) {
    controller
        .set_power_saving(esp_radio::wifi::PowerSaveMode::None)
        .unwrap();

    let client_config = ModeConfig::Client(
        ClientConfig::default()
            .with_ssid(SSID.into())
            .with_password(PASSWORD.into()),
    );
    let res = controller.set_config(&client_config);
    println!("wifi_set_configuration returned {:?}", res);

    controller.start().unwrap();
    println!("is wifi started: {:?}", controller.is_started());
}
}

Сканирование Wi-Fi и подключение к точке доступа

Далее выполняем блокирующее сканирование Wi-Fi-сетей с параметрами по умолчанию. Это значит, что программа остановится и будет ждать, пока сканирование не завершится. Когда сканирование закончится, будет возвращён список доступных точек доступа Wi-Fi. Затем мы проходим по результатам и выводим информацию о каждой точке доступа в консоль.

#![allow(unused)]
fn main() {
fn scan_wifi(controller: &mut WifiController<'_>) {
    println!("Start Wifi Scan");
    let scan_config = ScanConfig::default().with_max(10);
    let res = controller.scan_with_config(scan_config).unwrap();
    for ap in res {
        println!("{:?}", ap);
    }
}
}

Подключение к Wi-Fi

Затем выводим поддерживаемые возможности контроллера: будет показан только «Client», так как именно его мы и настроили.

После этого вызываем метод connect у контроллера Wi-Fi. Это подключит ESP32 к сети Wi-Fi, которую мы указали ранее.

В цикле будем постоянно проверять, подключён ли Wi-Fi. Как только подключение установится, перейдём дальше.

#![allow(unused)]
fn main() {
fn connect_wifi(controller: &mut WifiController<'_>) {
    println!("{:?}", controller.capabilities());
    println!("wifi_connect {:?}", controller.connect());

    println!("Wait to get connected");
    loop {
        match controller.is_connected() {
            Ok(true) => break,
            Ok(false) => {}
            Err(err) => panic!("{:?}", err),
        }
    }
    println!("Connected: {:?}", controller.is_connected());
}
}

Получение IP-адреса

Нужно получить IP-адрес, поэтому сначала ждём завершения процесса DHCP. Будем в цикле проверять, поднялся ли интерфейс и получили ли мы IP-адрес.

#![allow(unused)]
fn main() {
fn obtain_ip(stack: &mut Stack<'_, esp_radio::wifi::WifiDevice<'_>>) {
    println!("Wait for IP address");
    loop {
        stack.work();
        if stack.is_iface_up() {
            println!("IP acquired: {:?}", stack.get_ip_info());
            break;
        }
    }
}
}

На этом этапе ESP32 должен быть успешно подключён к сети Wi-Fi. Далее настроим код, который отправит HTTP-запрос к сайту. После отправки запроса ESP32 получит HTML-ответ от сайта, и мы выведем HTML-содержимое в консоль.

HTTP-клиент

Чтобы отправить веб-запрос на сайт, нужен HTTP-клиент. Для этого мы используем крейт smoltcp, чтобы отправить «сырой» HTTP-запрос через TCP-сокет.

«smoltcp — это самостоятельный управляемый событиями стек TCP/IP, предназначенный для bare-metal и систем реального времени. smoltcp вообще не требует выделения кучи, обширно документирован и компилируется на стабильном Rust 1.80 и новее».

TCP-сокет

Инициализируем TCP-сокет с буферами чтения и записи для входящих и исходящих данных сокета.

#![allow(unused)]
fn main() {
let mut rx_buffer = [0u8; 1536];
let mut tx_buffer = [0u8; 1536];
let socket = stack.get_socket(&mut rx_buffer, &mut tx_buffer);

http_loop(socket)
}

Цикл HTTP: отправка GET-запроса

Отправим HTTP-запрос методом GET на сайт «www.mobile-j.de». Этот сайт используется в примерах esp-hal: он возвращает «Hello fellow Rustaceans!» с ASCII-арт Ferris. Откроем соединение на порту 80 по IP-адресу сайта и затем отправим HTTP-запрос.

#![allow(unused)]
fn main() {
println!("Making HTTP request");
socket.work();

let remote_addr = IpAddress::v4(142, 250, 185, 115);
socket.open(remote_addr, 80).unwrap();
socket
    .write(b"GET / HTTP/1.0\r\nHost: www.mobile-j.de\r\n\r\n")
    .unwrap();
socket.flush().unwrap();
}

Чтение ответа

Далее читаем ответ сервера с таймаутом 20 секунд.

#![allow(unused)]
fn main() {
let deadline = Instant::now() + Duration::from_secs(20);
let mut buffer = [0u8; 512];
while let Ok(len) = socket.read(&mut buffer) {
    // let text = unsafe { core::str::from_utf8_unchecked(&buffer[..len]) };
    let Ok(text) = core::str::from_utf8(&buffer[..len]) else {
        panic!("Invalid UTF-8 sequence encountered");
    };

    println!("{}", text);

    if Instant::now() > deadline {
        println!("Timeout");
        break;
    }
}
}

Закрытие сокета

После завершения HTTP-запроса закрываем сокет и ждём 5 секунд.

#![allow(unused)]
fn main() {
socket.disconnect();
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
    socket.work();
}
}

Клонирование существующего проекта

Вы также можете клонировать (или посмотреть) созданный мной проект и перейти в папку wifi-webfetch.

git clone https://github.com/ImplFerris/esp32-projects
cd esp32-projects/wifi-webfetch

Как запустить?

Обычно мы просто запускаем cargo run --release, но на этот раз нужно также передать переменные окружения для подключения к Wi-Fi.

SSID=YOUR_WIFI_NAME PASSWORD=YOUR_WIFI_PASSWORD  cargo run --release

Полный код

#![no_std]
#![no_main]
#![deny(
    clippy::mem_forget,
    reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
    holding buffers for the duration of a data transfer."
)]

use blocking_network_stack::Stack;
use embedded_io::{Read, Write};
use esp_hal::clock::CpuClock;
use esp_hal::delay::Delay;
use esp_hal::main;
use esp_hal::peripherals::Peripherals;
use esp_hal::rng::Rng;
use esp_hal::time::{Duration, Instant};
use esp_hal::timer::timg::TimerGroup;
use esp_println::{self as _, println};
use esp_radio::wifi::{ClientConfig, ModeConfig, ScanConfig, WifiController};
use smoltcp::iface::{SocketSet, SocketStorage};
use smoltcp::wire::{DhcpOption, IpAddress};

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

extern crate alloc;

// Создаёт дескриптор приложения по умолчанию, который требуется загрузчику esp-idf.
// Подробнее см.: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();

const SSID: &str = env!("SSID");
const PASSWORD: &str = env!("PASSWORD");

#[main]
fn main() -> ! {
    // generator version: 1.0.0

    let peripherals = init_hardware();

    esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 98767);

    let timg0 = TimerGroup::new(peripherals.TIMG0);
    let rng = Rng::new();

    esp_rtos::start(timg0.timer0);
    let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller");

    let (mut wifi_controller, interfaces) =
        esp_radio::wifi::new(&radio_init, peripherals.WIFI, Default::default())
            .expect("Failed to initialize Wi-Fi controller");

    let mut device = interfaces.sta;

    // let mut stack = setup_network_stack(device, &mut rng);
    let mut socket_set_entries: [SocketStorage; 3] = Default::default();
    let mut socket_set = SocketSet::new(&mut socket_set_entries[..]);
    let mut dhcp_socket = smoltcp::socket::dhcpv4::Socket::new();

    // здесь можно задать имя хоста (или добавить другие DHCP-опции)
    dhcp_socket.set_outgoing_options(&[DhcpOption {
        kind: 12,
        data: b"implRust",
    }]);
    socket_set.add(dhcp_socket);
    // sta_socket_set.add(smoltcp::socket::dhcpv4::Socket::new());

    let now = || Instant::now().duration_since_epoch().as_millis();
    let mut stack = Stack::new(
        create_interface(&mut device),
        device,
        socket_set,
        now,
        rng.random(),
    );

    configure_wifi(&mut wifi_controller);
    scan_wifi(&mut wifi_controller);
    connect_wifi(&mut wifi_controller);
    obtain_ip(&mut stack);

    let mut rx_buffer = [0u8; 1536];
    let mut tx_buffer = [0u8; 1536];
    let socket = stack.get_socket(&mut rx_buffer, &mut tx_buffer);

    http_loop(socket)
}

pub fn create_interface(device: &mut esp_radio::wifi::WifiDevice) -> smoltcp::iface::Interface {
    // можно создать несколько экземпляров, но так как WifiDevice один, сделать с ним что-то плохое вряд ли получится
    // скорее всего, с ним нельзя сделать ничего плохого
    smoltcp::iface::Interface::new(
        smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ethernet(
            smoltcp::wire::EthernetAddress::from_bytes(&device.mac_address()),
        )),
        device,
        timestamp(),
    )
}

// шаблонный код smoltcp
fn timestamp() -> smoltcp::time::Instant {
    smoltcp::time::Instant::from_micros(
        esp_hal::time::Instant::now()
            .duration_since_epoch()
            .as_micros() as i64,
    )
}

fn init_hardware() -> Peripherals {
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);
    esp_alloc::heap_allocator!(size: 72 * 1024);
    peripherals
}

fn configure_wifi(controller: &mut WifiController<'_>) {
    controller
        .set_power_saving(esp_radio::wifi::PowerSaveMode::None)
        .unwrap();

    let client_config = ModeConfig::Client(
        ClientConfig::default()
            .with_ssid(SSID.into())
            .with_password(PASSWORD.into()),
    );
    let res = controller.set_config(&client_config);
    println!("wifi_set_configuration returned {:?}", res);

    controller.start().unwrap();
    println!("is wifi started: {:?}", controller.is_started());
}

fn scan_wifi(controller: &mut WifiController<'_>) {
    println!("Start Wifi Scan");
    let scan_config = ScanConfig::default().with_max(10);
    let res = controller.scan_with_config(scan_config).unwrap();
    for ap in res {
        println!("{:?}", ap);
    }
}

fn connect_wifi(controller: &mut WifiController<'_>) {
    println!("{:?}", controller.capabilities());
    println!("wifi_connect {:?}", controller.connect());

    println!("Wait to get connected");
    loop {
        match controller.is_connected() {
            Ok(true) => break,
            Ok(false) => {}
            Err(err) => panic!("{:?}", err),
        }
    }
    println!("Connected: {:?}", controller.is_connected());
}

fn obtain_ip(stack: &mut Stack<'_, esp_radio::wifi::WifiDevice<'_>>) {
    println!("Wait for IP address");
    loop {
        stack.work();
        if stack.is_iface_up() {
            println!("IP acquired: {:?}", stack.get_ip_info());
            break;
        }
    }
}

fn http_loop(
    mut socket: blocking_network_stack::Socket<'_, '_, esp_radio::wifi::WifiDevice<'_>>,
) -> ! {
    println!("Starting HTTP client loop");
    let delay = Delay::new();
    loop {
        println!("Making HTTP request");
        socket.work();

        let remote_addr = IpAddress::v4(142, 250, 185, 115);
        socket.open(remote_addr, 80).unwrap();
        socket
            .write(b"GET / HTTP/1.0\r\nHost: www.mobile-j.de\r\n\r\n")
            .unwrap();
        socket.flush().unwrap();

        let deadline = Instant::now() + Duration::from_secs(20);
        let mut buffer = [0u8; 512];
        while let Ok(len) = socket.read(&mut buffer) {
            // let text = unsafe { core::str::from_utf8_unchecked(&buffer[..len]) };
            let Ok(text) = core::str::from_utf8(&buffer[..len]) else {
                panic!("Invalid UTF-8 sequence encountered");
            };

            println!("{}", text);

            if Instant::now() > deadline {
                println!("Timeout");
                break;
            }
        }

        socket.disconnect();
        let deadline = Instant::now() + Duration::from_secs(5);
        while Instant::now() < deadline {
            socket.work();
        }

        delay.delay_millis(1000);
    }
}