Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rethink WiFi API #3027

Merged
merged 2 commits into from
Feb 14, 2025
Merged

Rethink WiFi API #3027

merged 2 commits into from
Feb 14, 2025

Conversation

bjoernQ
Copy link
Contributor

@bjoernQ bjoernQ commented Jan 24, 2025

Thank you for your contribution!

We appreciate the time and effort you've put into this pull request.
To help us review it efficiently, please ensure you've gone through the following checklist:

Submission Checklist 📝

  • I have updated existing examples or added new ones (if applicable).
  • I have used cargo xtask fmt-packages command to ensure that all changed code is formatted correctly.
  • My changes were added to the CHANGELOG.md in the proper section.
  • I have added necessary changes to user code to the Migration Guide.
  • My changes are in accordance to the esp-rs API guidelines

Extra:

Pull Request Details 📖

Description

This is a POC to address #2224

The idea is

  • network devices / interfaces are mostly independent of the controller, but both get created at the same time
  • no type-state for the enabled wifi mode - a user can switch the wifi mode at any time
  • there might be a next step turning sniffer and esp-now into "interfaces"

Testing

Run the examples

@bjoernQ
Copy link
Contributor Author

bjoernQ commented Feb 4, 2025

I don't think adding an example is interesting enough but here is the wifi_dhcp example modified to first start as STA, do one request and then switches to AP

Example changed to start as STA, does one request and starts an AP
//! DHCP Example
//!
//!
//! Set SSID and PASSWORD env variable before running this example.
//!
//! This gets an ip address via DHCP then performs an HTTP get request to some "random" server
//!

//% FEATURES: esp-wifi esp-wifi/wifi  esp-hal/unstable esp-wifi/smoltcp
//% CHIPS: esp32 esp32s2 esp32s3 esp32c2 esp32c3 esp32c6

// esp-wifi/utils

#![no_std]
#![no_main]

extern crate alloc;
use core::net::Ipv4Addr;

use blocking_network_stack::Stack;
use embedded_io::*;
use esp_alloc as _;
use esp_backtrace as _;
use esp_hal::{
    clock::CpuClock,
    main,
    rng::Rng,
    time::{self, Duration},
    timer::timg::TimerGroup,
};
use esp_println::{print, println};
use esp_wifi::{
    init,
    wifi::{AccessPointConfiguration, AccessPointInfo, ClientConfiguration, Configuration, WifiError},
};
use smoltcp::{
    iface::{SocketSet, SocketStorage},
    wire::{DhcpOption, IpAddress},
};

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

#[main]
fn main() -> ! {
    esp_println::logger::init_logger_from_env();
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);

    esp_alloc::heap_allocator!(72 * 1024);

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

    let mut rng = Rng::new(peripherals.RNG);

    let esp_wifi_ctrl = init(timg0.timer0, rng.clone(), peripherals.RADIO_CLK).unwrap();

    let (mut controller, interfaces) =
        esp_wifi::wifi::new(&esp_wifi_ctrl, peripherals.WIFI).unwrap();

    {
        let mut device = interfaces.sta;
        let iface = device.interface();

        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();
        // we can set a hostname here (or add other DHCP options)
        dhcp_socket.set_outgoing_options(&[DhcpOption {
            kind: 12,
            data: b"esp-wifi",
        }]);
        socket_set.add(dhcp_socket);

        let now = || time::now().duration_since_epoch().to_millis();
        let stack = Stack::new(iface, device, socket_set, now, rng.random());

        controller
            .set_power_saving(esp_wifi::config::PowerSaveMode::None)
            .unwrap();

        let client_config = Configuration::Client(ClientConfiguration {
            ssid: SSID.try_into().unwrap(),
            password: PASSWORD.try_into().unwrap(),
            ..Default::default()
        });
        let res = controller.set_configuration(&client_config);
        println!("wifi_set_configuration returned {:?}", res);

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

        println!("Start Wifi Scan");
        let res: Result<(heapless::Vec<AccessPointInfo, 10>, usize), WifiError> =
            controller.scan_n();
        if let Ok((res, _count)) = res {
            for ap in res {
                println!("{:?}", ap);
            }
        }

        println!("{:?}", controller.capabilities());
        println!("wifi_connect {:?}", controller.connect());

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

        // wait for getting an ip address
        println!("Wait to get an ip address");
        loop {
            stack.work();

            if stack.is_iface_up() {
                println!("got ip {:?}", stack.get_ip_info());
                break;
            }
        }

        println!("Start busy loop on main");

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

        loop {
            println!("Making HTTP request");
            socket.work();

            socket
                .open(IpAddress::Ipv4(Ipv4Addr::new(142, 250, 185, 115)), 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 = time::now() + Duration::secs(20);
            let mut buffer = [0u8; 512];
            while let Ok(len) = socket.read(&mut buffer) {
                let to_print = unsafe { core::str::from_utf8_unchecked(&buffer[..len]) };
                print!("{}", to_print);

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

            socket.disconnect();

            let deadline = time::now() + Duration::secs(5);
            while time::now() < deadline {
                socket.work();
            }

            break;
        }
    }

    {
        let mut device = interfaces.ap;
        let iface = device.interface();
    
        let now = || time::now().duration_since_epoch().to_millis();
    
        let mut socket_set_entries: [SocketStorage; 3] = Default::default();
        let socket_set = SocketSet::new(&mut socket_set_entries[..]);
        let mut stack = Stack::new(iface, device, socket_set, now, rng.random());
    
        let ap_config = Configuration::AccessPoint(AccessPointConfiguration {
            ssid: "esp-wifi".try_into().unwrap(),
            ..Default::default()
        });
        let res = controller.set_configuration(&ap_config);
        println!("wifi_set_configuration returned {:?}", res);
    
        controller.start().unwrap();
        println!("is wifi started: {:?}", controller.is_started());
    
        println!("{:?}", controller.capabilities());
    
        stack
            .set_iface_configuration(&blocking_network_stack::ipv4::Configuration::Client(
                blocking_network_stack::ipv4::ClientConfiguration::Fixed(
                    blocking_network_stack::ipv4::ClientSettings {
                        ip: blocking_network_stack::ipv4::Ipv4Addr::from(parse_ip("192.168.2.1")),
                        subnet: blocking_network_stack::ipv4::Subnet {
                            gateway: blocking_network_stack::ipv4::Ipv4Addr::from(parse_ip(
                                "192.168.2.1",
                            )),
                            mask: blocking_network_stack::ipv4::Mask(24),
                        },
                        dns: None,
                        secondary_dns: None,
                    },
                ),
            ))
            .unwrap();
    
        println!("Start busy loop on main. Connect to the AP `esp-wifi` and point your browser to http://192.168.2.1:8080/");
        println!("Use a static IP in the range 192.168.2.2 .. 192.168.2.255, use gateway 192.168.2.1");
    
        let mut rx_buffer = [0u8; 1536];
        let mut tx_buffer = [0u8; 1536];
        let mut socket = stack.get_socket(&mut rx_buffer, &mut tx_buffer);
    
        socket.listen(8080).unwrap();
    
        loop {
            socket.work();
    
            if !socket.is_open() {
                socket.listen(8080).unwrap();
            }
    
            if socket.is_connected() {
                println!("Connected");
    
                let mut time_out = false;
                let deadline = time::now() + Duration::secs(20);
                let mut buffer = [0u8; 1024];
                let mut pos = 0;
                while let Ok(len) = socket.read(&mut buffer[pos..]) {
                    let to_print = unsafe { core::str::from_utf8_unchecked(&buffer[..(pos + len)]) };
    
                    if to_print.contains("\r\n\r\n") {
                        print!("{}", to_print);
                        println!();
                        break;
                    }
    
                    pos += len;
    
                    if time::now() > deadline {
                        println!("Timeout");
                        time_out = true;
                        break;
                    }
                }
    
                if !time_out {
                    socket
                        .write_all(
                            b"HTTP/1.0 200 OK\r\n\r\n\
                        <html>\
                            <body>\
                                <h1>Hello Rust! Hello esp-wifi!</h1>\
                            </body>\
                        </html>\r\n\
                        ",
                        )
                        .unwrap();
    
                    socket.flush().unwrap();
                }
    
                socket.close();
    
                println!("Done\n");
                println!();
            }
    
            let deadline = time::now() + Duration::secs(5);
            while time::now() < deadline {
                socket.work();
            }
        }        
    }

}


fn parse_ip(ip: &str) -> [u8; 4] {
    let mut result = [0u8; 4];
    for (idx, octet) in ip.split(".").into_iter().enumerate() {
        result[idx] = u8::from_str_radix(octet, 10).unwrap();
    }
    result
}

@MabezDev
Copy link
Member

MabezDev commented Feb 4, 2025

I don't think adding an example is interesting enough but here is the wifi_dhcp example modified to first start as STA, do one request and then switches to AP

I think this is looking better! I have a few questions:

  • What happens when a user wants to do AP_STA mode?
  • Can both interfaces be active at the same time?
  • This one might be a bit harder, but can we internalize these different interfaces?
    • In theory AP and STA will have a different address, so we could have some intermediate "device" that can do some limited packet parsing to route the packet to the correct interface?

@bjoernQ
Copy link
Contributor Author

bjoernQ commented Feb 4, 2025

  • What happens when a user wants to do AP_STA mode?

We have the wifi_embassy_access_point_with_sta which works perfectly fine with this

  • Can both interfaces be active at the same time?

Yes - the above example does that

  • This one might be a bit harder, but can we internalize these different interfaces?

    • In theory AP and STA will have a different address, so we could have some intermediate "device" that can do some limited packet parsing to route the packet to the correct interface?

I guess that would be asking for trouble and support-tickets. Besides it's definitely not easy (need to parse and inspect ARP packages, keep a routing table in addition to what smoltcp does internally (if we can't access that) it would make things harder for the user if the networks happen to have an overlapping IP address range. Big OSes have a way to bind and connect sockets by specifying an interface (well Rust's std-lib doesn't and need additional 3rd party libraries) but smoltcp can't - so can't embassy-net and similar stacks

In most scenarios users won't gain anything from that (besides trouble and tears) since (my assumption) they usually will use AP just for provisioning and switch to AP then.

If they want something like that (e.g. building a router) they can do that on their own by creating such a combined smoltcp device - maybe we also should expose raw sending/receiving so people are not limited to smoltcp (but that's way beyond the scope here)

@Dominaezzz
Copy link
Collaborator

  • so we could have some intermediate "device" that can do some limited packet parsing to route the packet to the correct interface?

I don't think this is generally possible, it'd only work in some scenarios, like only if sta and ap are in different subnets, and the esp32 is the gateway of the ap interface, and both interfaces are using the same layer 2 protocol (IPv4 Vs ipv6), etc.
I've had trouble getting this right with Linux even, probably not worth the trouble attempting it here

@MabezDev
Copy link
Member

MabezDev commented Feb 6, 2025

Maybe I was a bit too ambitious with the routing point, but I think its still worth trying to internalize these interfaces. If the WiFi driver holds all interfaces, we can just expose change_mode() function where mode is sta, ap, ap_sta, and here we can swap the "active" interface based on the mode?

@bjoernQ
Copy link
Contributor Author

bjoernQ commented Feb 6, 2025

Maybe I was a bit too ambitious with the routing point, but I think its still worth trying to internalize these interfaces. If the WiFi driver holds both, we can just expose change_mode() function where mode is sta, ap, ap_sta, and here we can swap the "active" interface based on the mode?

change_mode would need the WiFi credentials and then it will look like set_configuration - I guess set_configuration is a better name then

We could have one interface either for sta or ap (IIRC we had that in the beginning) but for ap_sta we run into the same problems (i.e. remembering which hosts are reachable on which interface, address collision, ...)

We could explore that in a follow up step (but I'm not too optimistic we will get something sane for AP_STA)

@bjoernQ bjoernQ marked this pull request as ready for review February 6, 2025 12:22
@@ -1629,6 +1625,7 @@ pub(crate) fn wifi_start() -> Result<(), WifiError> {
cntry_code[2] = crate::CONFIG.country_code_operating_class;

let country = wifi_country_t {
#[allow(clippy::useless_transmute)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add a FIXME to remove this after the next rustc release, the c_char redefinition should be stable in 1.85. Needs an MSRV bump for sure, but I'd rather not carry around a transmute when we don't need it.

Copy link
Contributor

@bugadani bugadani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this PR is borderline unreviewable on the github diff page, but here are a few comments.

@bjoernQ bjoernQ force-pushed the esp-wifi/wifi-api branch 2 times, most recently from ce74ffb to a9d6b15 Compare February 7, 2025 12:02
Comment on lines 331 to 334
#[embassy_executor::task]
async fn ap_task(mut runner: Runner<'static, WifiDevice<'static, WifiApDevice>>) {
async fn ap_task(mut runner: Runner<'static, WifiDevice<'static>>) {
runner.run().await
}

#[embassy_executor::task]
async fn sta_task(mut runner: Runner<'static, WifiDevice<'static, WifiStaDevice>>) {
async fn sta_task(mut runner: Runner<'static, WifiDevice<'static>>) {
runner.run().await
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note; Now that Generics have been removed, can this now become:

#[embassy_executor::task(pool_size = 2)]
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
    runner.run().await
}

and spawn the same task twice instead of defining two different tasks?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At that point this can also turn into something like:

#[embassy_executor::task(pool_size = 2]
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
    runner.run().await
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I'm meant to be playing a game of spot the difference here, but aren't they the same? :D

Copy link
Contributor

@bugadani bugadani Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently I'm just blind and I just saw the diff, but I actually made a syntax error so there is a difference 🙈

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I actually made a syntax error so there is a difference 🙈

Aww, that means I lost :(

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference then? 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing closing bracket on task pool (yes it still took me another good 5 mins to find it 🤣)

@bjoernQ bjoernQ force-pushed the esp-wifi/wifi-api branch 2 times, most recently from 57e5f7b to 3f58873 Compare February 11, 2025 10:40
Copy link
Member

@MabezDev MabezDev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice improvement, LGTM!

@bugadani
Copy link
Contributor

You'll need to rebase the PR over recent xtask changes, but otherwise LGTM.

@bjoernQ bjoernQ added this pull request to the merge queue Feb 14, 2025
Merged via the queue into esp-rs:main with commit 0138d46 Feb 14, 2025
27 checks passed
}

/// Get the currently used configuration.
pub fn configuration(&self) -> Result<Configuration, WifiError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed, what's the alternative and why isn't this documented?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for the need for a better migration guide

I never considered this very useful - it was there because the embedded-svc had it but then we removed it - now we don't store the config anymore and doing it just to provide a getter seemed to be a bad idea (i.e. storing it just to provide a getter for something set by the user)

But again - we need to talk about the changes more in the MG

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants