-
Notifications
You must be signed in to change notification settings - Fork 264
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
Rethink WiFi API #3027
Conversation
1bfa762
to
4bffeee
Compare
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
} |
I think this is looking better! I have a few questions:
|
We have the
Yes - the above example does that
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) |
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. |
d8a8fba
to
b92e717
Compare
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 |
We could have one interface either for We could explore that in a follow up step (but I'm not too optimistic we will get something sane for AP_STA) |
@@ -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)] |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
ce74ffb
to
a9d6b15
Compare
#[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 | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 🙈
There was a problem hiding this comment.
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 :(
There was a problem hiding this comment.
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? 😅
There was a problem hiding this comment.
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 🤣)
57e5f7b
to
3f58873
Compare
There was a problem hiding this 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!
You'll need to rebase the PR over recent xtask changes, but otherwise LGTM. |
3f58873
to
c5d3672
Compare
} | ||
|
||
/// Get the currently used configuration. | ||
pub fn configuration(&self) -> Result<Configuration, WifiError> { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
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 📝
cargo xtask fmt-packages
command to ensure that all changed code is formatted correctly.CHANGELOG.md
in the proper section.Extra:
Pull Request Details 📖
Description
This is a POC to address #2224
The idea is
Testing
Run the examples