-
Notifications
You must be signed in to change notification settings - Fork 194
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
Add WiFi Easy Connect (DPP) wrappers and associated example #228
base: master
Are you sure you want to change the base?
Conversation
Having troubles getting the example to build for unknown reasons. I have things working in my own local repository, but for some reason sdkconfig.defaults isn't being picked up like I expect for the examples. I can ofc just remove it, but that seems like a shame since this really helped me figure out a good path for provisioning without hardcoding stuff. |
Also looks like I'm missing some conventions about using alloc vs heapless, std::fmt::Write, etc. Fairly new to the conventions in esp-idf-svc but I'll do my best to try to fix things up. Pointers welcome :) |
Cargo.toml
Outdated
@@ -30,8 +30,8 @@ log = { version = "0.4", default-features = false } | |||
uncased = "0.9.7" | |||
anyhow = { version = "1", default-features = false, optional = true } # Only used by the deprecated httpd module | |||
embedded-svc = { version = "0.24", default-features = false } | |||
esp-idf-sys = { version = "0.32.1", default-features = false, features = ["native"] } |
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.
Oops, will remove :)
src/wifi_dpp.rs
Outdated
} | ||
|
||
pub struct EspDppBootstrapped<'d, T> { | ||
events_rx: &'d Receiver<DppEvent>, |
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.
Hmm, this should probably be &mut so that it guarantees the bootstrapper binding cannot be used while the bootstrapped binding is in scope.
Sorry for the delay here. Will look at it over the weekend. |
I think I might want to rework this a bit so that there's only one struct, EspDppBootstrapped, and that the borrow of EspWifi only occurs during the constructor. This way you could have a separate thread call EspWifi::stop while EspDppBootstrapped is "stuck" in listen_forever/listen_once() and have it be interrupted. I believe this would occur because esp_supp_dpp_* internally is implemented to listen for wifi events and bail out on error. This would also make the code a bit more ergonomic to use in practice as all the lifetime stuff would disappear from the struct definition. I will also remove the example unless ya'll have a good way to keep it and have it actually compile correctly given that it will need sdkconfig.defaults to be provided and I'm not at all sure how that is supposed to work in practice (how can each example have its own config???) |
Actually I just realized that if I do this so that EspWifi isn't borrowed for the lifetime of the object that handles esp_supp_dpp_init/deinit then we could end up unsafely accessing the singleton (e.g. calling esp_supp_dpp_init twice). The only solution I can think of there is to add a new peripheral. That looks like a more invasive change for sure, and would like to get feedback before I proceed. |
What about merging the DPP support inside By the way: your current PR relies on STD being around, including using the |
Ackd, I think the right compromise here is to have a member of EspWifi that you can borrow to access esp_supp_dpp_*. I'll still keep the code in wifi_dpp.rs so it's neatly separated. That said, I'm still waffling on whether it's better to mutable borrow EspWifi while esp_supp_dpp is in use. There definitely seems to be a few APIs that wouldn't be safe like connect while listening. It does at least seem safe to use EspWifi while you're not actively listening tho (generating the QR code doesn't seem to even utilize the wifi driver, just some generic code in wpa_supplicant). Re std, yes I'm aware that I need to drop that, I'm just not 100% sure what the proper way to do it is. I'll dig deeper into the rest of the code to see if I can't find prior examples of similar callback relaying going on. If you've got any pointers tho that'd be appreciated to save me some time :) |
Oh and I'd like some advice on the example I originally had. Can't get it to compile because it needs sdkconfig values and some other project weirdness. Is it preferable to put the example in some other repo? |
Borrowing mutably
Well, a lot of the stuff in STD is either type aliases to stuff which is actually in If you really really need to reach out for other STD stuff (as in networking, FS, multithreading, or complex stuff with mutexes and condvars, you probably need to rethink all of it). If you really really need mutexes and condvars, we have these in |
Thanks, I'll iterate a bit on my end and update the PR when it's ready for you to look again. FWIW, the DPP lifecycle is pretty mysterious from the documentation and even implementation. It's almost like it can coexist simultaneously with the rest of the Wi-Fi functionality but there's glaring conflicts at the edges. What I wasn't sure about was whether this project prefers to be conservative (the Rust wrappers can't reasonably break in unsafe ways, e.g. segfault) or liberal (the Rust wrappers use convenient idioms but are roughly as dangerous/powerful as the C APIs). I'm currently assuming the former. |
Absolutely the former. If a Rust API is marked as "safe", then it should not break, period. :) |
@ivmarkov, updated as an RFC with a few caveats:
|
} | ||
|
||
// TODO: Sure would be nice to be able to write esp_idf_version >= 5.1... | ||
#[cfg(any( |
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.
Note: I could do this at runtime for much less code, but I'm not sure how to access the version from esp-idf-sys? Is ESP_IDF_VERSION/_VAL exposed?
/// Blocking wait for credentials or a possibly retryable error. Note that user error | ||
/// such as scanning the wrong QR code can trigger this error case. Retries are highly | ||
/// recommended, and especially via [Self::attempt_retry]. | ||
pub fn wait_for_credentials(&self) -> Result<ClientConfiguration, EspError> { |
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 hesitated on this being &mut self or &self. It will break stuff if you call this in parallel (should hang forever), but if it's &mut self we can't expose stop_listen on Self. Would it be preferred to split the listener into two structs, one that can be used to cancel and one that can be used to wait with some shared lock between them?
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 fine with the current little cheat for now. We can fix/complicate in a subsequent version.
I think the comparison you do above might be slightly misleading. The way I understand it, is that Rust's current What Rust doesn't have is the new "pure"
Waiting for Rust to do - whatever - just to allow folks to use ESP IDF 5.1 seems like the wrong choice for me. It is absolutely not so simple to add the new targets.
Why not? Given the situation, it seems this is the cleanest option, until everyone had upgraded?
If we can't do (2) from above, we should be doing (3) IMO. So my order in terms of feasibility would be: |
@ivmarkov i think your above comment was from a different issue I filed. On my mobile now and can't find the link easily but it relates to building esp idf 5.1. we can consider this PR about DPP separate. I'll wait to respond in full on the other task for a clearer paper trail (but i do agree with the gist of what you said). Fwiw they were related in the sense that I had to patch esp-idf which meant I had to build and test master which ofc broke :) |
@jasta Yes, sorry for the noise. I've moved my comment to the issue here: esp-rs/esp-idf-sys#176 |
/// esp_supp_dpp_init. | ||
static DPP_INITIALIZED: Mutex<Option<Weak<DppInitialized>>> = Mutex::wrap(RawMutex::new(), None); | ||
|
||
impl DppInitialized { |
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 guess I'm missing a detail here, but I'm a bit lost as to why we need DppInitialized
in the first place? Here's my thinking:
EspWifi
is a singleton; you cannot have two instances of it, alive at any time- Ergo, there can be only one instance, ever of
EspWifiDpp
alive at any time, as by design, creating such an instance needs a mutable borrow ofEspWifi
- You initialize DPP when constructing
EspWifiDpp
- You de-initialize DPP when dropping
EspWifiDpp
What am I missing?
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 can combine them, but the main reason I kept them separate was so that it was very clear the global singleton (DPP_INITIALIZED) was literally tied to the esp_supp_dpp_init/deinit lifecycle, whereas EspWifiDpp was tied to the bootstrapped lifecycle after you call esp_supp_dpp_bootstrap_gen. In theory, you could bootstrap multiple times without deinit which would require fewer changes with the way it's currently designed than it would if DppInitialized/EspWifiDpp were combined. I'll leave it up to you if you prefer to reduce the total amount of code further in exchange for a maybe heavier refactoring needed later if we want to add features (which I suspect will happen since Espressif's support for DPP is pretty lacking at the moment).
/// ESP-IDF is being used. | ||
/// * `associated_data` - (Optional) Arbitrary extra information to include with the QR | ||
/// code that may be relevant to the configurator. | ||
pub fn generate_qrcode( |
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 not naming it simply... new
?
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.
In the future I expect there to be other ways to bootstrap DPP, as DPP itself supports QR code, proof of knowledge, NFC URI, and even Bluetooth in later revisions. Espressif's API even supports this idea through an awkward enum, though I'd prefer the Rust API expose this with new fn's like fn using_pkex(...)
, fn using_ble(...)
, etc.
|
||
#[cfg(not(esp_idf_version_major = "4"))] | ||
/// ESP-IDF 5.x requires the caller put the key into the PEM format | ||
fn frame_key(unframed: &[u8; 32]) -> Vec<u8> { |
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 calling into alloc
just for this? You can either return a heapless::Vec
(as the size of the returned thing is actually known), or even an array, as the final array size is also known?
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 was struggling with this one a bit going back and forth between a compile-time approach vs a runtime one (compile-time can do as you say with heapless, runtime would need alloc since you wouldn't know the result size). Since I settled on compile-time, I'll go ahead and slip in heapless::Vec
again.
/// Blocking wait for credentials or a possibly retryable error. Note that user error | ||
/// such as scanning the wrong QR code can trigger this error case. Retries are highly | ||
/// recommended, and especially via [Self::attempt_retry]. | ||
pub fn wait_for_credentials(&self) -> Result<ClientConfiguration, EspError> { |
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 fine with the current little cheat for now. We can fix/complicate in a subsequent version.
@jasta Very sorry for having you wait for so long for my feedback! |
No worries, I understand the need to take this slow on new APIs, especially since it's not cut and dry how exactly to port C APIs to Rust idioms. Also note I'm not in a hurry to land this just yet as Espressif still hasn't accepted or even really acknowledged my patch that actually makes the feature work reliably: espressif/esp-idf#10865. Without the patch, even their own example code doesn't work better than 1/10 times for me. |
8a8adba
to
5ccb542
Compare
On the other hand, Espressif's own provisioning works well over BLE, has apps and documentation for Android and iOS, and importantly, supports custom endpoints. It may be a better candidate to implement over DPP. |
You can always take the time and open a PR for it then! DPP is at least a standard albeit not widely accepted. This means eventually you might get support for it out of the box in your phone, with a built-in app. The Espressif provisioning is done with their own, Espressif-branded app, that you have to fork rebrand and maintain. If you do this, you might be better off just maintaining your own, DPP-compatible app. At least on Android, the DPP APIs are available. Not sure for iPhone. |
If you do not need custom endpoints, for hobby projects, you can download Espressiff-branded app from the Play Store/App store. No forking/rebranding necessary. As for building non-hobby projects, firmware could require custom configuration in some way on first boot, so custom endpoints are a must. DPP lacks in that regard. |
I have no idea what endpoints or custom endpoints even mean in the context of wifi provisioning, so I don't follow. |
Custom endpoints allow exchange of extra configuration during provisioning process besides just wifi credentials. One example could be setting MQTT url during provisioning process. I am unclear how DPP-based provisioning would accomplish this. |
This might not be necessary at all in certain cases, like most / all B2C use-cases, where the MQTT server endpoint is simply hard-coded in the firmware or flashed in the device NVS storage during factory setup. Most end-user equipment only needs access to the internet. You can't ask your customer to type-in an MQTT URL, as she might not even know what is an MQTT URL in the first place. B2B is another story. There, having the option to provision extra info as part of the Wifi provisioning might or might not be useful. If your devices are hitting the cloud directly, surely not useful. If you have large enterprises with their own MQTT or whatever, surely useful yes. For a hobby setup, I don't think any of that (including the Espressif custom provisioner) is necessary. You could just hard-code all of that or run the good old soft-ap + captive portal + HTTP server combo. |
But with that said, and to repeat:
Unlike the bare-metal crates, the development of |
I have very basic implementation here - please let me know if you think this is worth doing a PR for. |
No description provided.