rainmaker_components/
wifi_prov.rs

1//! WiFi Provisioning component
2//!
3//! Provisioning is the process of providing WiFi credentials to a device without manually hardcoding credentials in firmware.
4//! WiFi Provisioning is built upon [Protocomm] using endpoints tailored for connecting nearby WiFi networks using
5//! companion phone apps.
6//!
7//! It is roughly based on the architecture of  WiFi provisioning component in ESP-IDF. More details can be found [here]
8//!
9//! It provides implementation for 2 transport methods(the initial communication between companion apps and the device):
10//! - Over BLE
11//! - Over SoftAP
12//!
13//! You can read more about each of the transports at: [WiFiProvMgrBle] and [WiFiProvMgrSoftAp]
14//!
15//! <b> Note: WiFi provisioning won't actually work for linux since WiFi component is WIP for linux. </b>
16//!
17//! Example
18//! ```rust
19//! let prov_config = WifiProvBleConfig {
20//!     service_name: String::from("PROV_SERVICE"),
21//!     ..Default::default()
22//! };
23//! let mut prov_mgr = WiFiProvMgrBle::new(
24//!     wifi_arc_mutex.clone(),
25//!     prov_config,
26//!     nvs_partition,
27//!     ProtocommSecurity::default(), // Will default to Sec0
28//! )?;
29//!
30//!
31//! rmaker.reg_user_mapping_ep(&mut prov_mgr);
32//! prov_mgr.start()?;
33//! ```
34//! Provisioning will stop when `prov_mgr` is dropped.
35//!
36//! [Protocomm]: crate::protocomm::Protocomm
37//! [here]: https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/provisioning/wifi_provisioning.html
38
39use std::sync::mpsc::{self, Receiver, Sender};
40
41use quick_protobuf::{MessageWrite, Writer};
42use serde_json::json;
43pub use uuid::Uuid;
44
45mod base;
46mod ble;
47mod softap;
48
49use crate::{
50    error::Error,
51    persistent_storage::{Nvs, NvsPartition},
52    protocomm::{ProtocommCallbackType, ProtocommSecurity},
53    utils::{wrap_in_arc_mutex, WrappedInArcMutex},
54    wifi::{WifiApInfo, WifiClientConfig, WifiMgr},
55};
56
57pub use base::WiFiProvTransportTrait;
58
59use ble::WiFiProvTransportBle;
60pub use ble::WifiProvBleConfig;
61
62pub use softap::WiFiProvSoftApConfig;
63use softap::WiFiProvTransportSoftAp;
64
65/// BLE transport for WiFi Provisioning.
66///
67/// Uses a GATT Service for provisioning purposes.
68/// The UUID for this Service is the one provided in the config.
69///
70/// Each of the endpoint is a GATT Characteristic.
71/// The UUID for the Characteristic is generated by masking 12th and 13th byte in Service UUID with increasing value for each endpoint registered.
72///
73/// Rest of the details are similar to BLE transport in [Protocomm]
74///
75/// <b> Note: Bluetooth needs to be turned on on Linux before running the program </b>
76///
77/// [Protocomm]: crate::protocomm::Protocomm
78pub type WiFiProvMgrBle = WifiProvMgr<WiFiProvTransportBle>;
79
80/// SoftAP transport for WiFi Provisioning.
81///
82/// It creates a WiFi access point and uses the Httpd transport provided by [Protocomm]
83/// Each of the endpoint is a HTTP endpoint which can be interacted with using POST requests with necessary data.
84///
85/// Rest of the details are similar to SoftAP transport in [Protocomm]
86///
87/// <b> Note: For using SoftAp transport on linux, you'll need to allow opening port 80(or run program as sudo) and externally create a WiFi access point on linux. </b>
88///
89/// [Protocomm]: crate::protocomm::Protocomm
90pub type WiFiProvMgrSoftAp = WifiProvMgr<WiFiProvTransportSoftAp>;
91
92const WIFI_NAMESPACE: &str = "net80211";
93const WIFI_SSID_KEY: &str = "sta.ssid";
94const WIFI_PASS_KEY: &str = "sta.pswd";
95
96// A struct for storing shared callback data between various callback functions
97struct ProvisioningSharedData {
98    wifi: WrappedInArcMutex<WifiMgr<'static>>,
99    scan_results: Option<Vec<WifiApInfo>>,
100    nvs_partition: NvsPartition,
101    msg_sender: Sender<()>,
102}
103
104pub struct WifiProvMgr<T>
105where
106    T: WiFiProvTransportTrait,
107{
108    sec_ver: u8,
109    pop: Option<String>,
110    shared: WrappedInArcMutex<ProvisioningSharedData>,
111    transport: T,
112    msg_receiver: Receiver<()>,
113}
114
115impl WiFiProvMgrSoftAp {
116    pub fn new(
117        wifi: WrappedInArcMutex<WifiMgr<'static>>,
118        config: WiFiProvSoftApConfig,
119        nvs_partition: NvsPartition,
120        sec: ProtocommSecurity,
121    ) -> Result<WiFiProvMgrSoftAp, Error> {
122        let (sec_ver, pop) = Self::get_sec_ver_and_pop(&sec);
123        let transport = WiFiProvTransportSoftAp::new(config, sec, wifi.clone());
124        Self::new_with_transport(wifi, nvs_partition, sec_ver, pop, transport)
125    }
126}
127
128impl WiFiProvMgrBle {
129    pub fn new(
130        wifi: WrappedInArcMutex<WifiMgr<'static>>,
131        config: WifiProvBleConfig,
132        nvs_partition: NvsPartition,
133        sec: ProtocommSecurity,
134    ) -> Result<WiFiProvMgrBle, Error> {
135        let (sec_ver, pop) = Self::get_sec_ver_and_pop(&sec);
136        let transport = WiFiProvTransportBle::new(config, sec);
137        Self::new_with_transport(wifi, nvs_partition, sec_ver, pop, transport)
138    }
139}
140
141impl<T: WiFiProvTransportTrait> WifiProvMgr<T> {
142    /// This function will block until node is not provisioned.
143    /// It will return once first time WiFi connection has been established using provisioned credentials.
144    pub fn wait_for_provisioning(&self) {
145        self.msg_receiver.recv().unwrap()
146    }
147
148    /// Adding additional endpoints to the pre-existing WiFi provisioning endpoints.
149    /// It is useful for an endpoint with application specific information and purpose.
150    pub fn add_endpoint(&mut self, ep_name: &str, cb: ProtocommCallbackType) {
151        self.transport.add_endpoint(ep_name, cb);
152    }
153
154    /// This starts the provisioning process.
155    pub fn start(&mut self) -> Result<(), Error> {
156        self.register_protocomm_endpoints();
157
158        self.transport.start()?;
159        let data = self.shared.lock().unwrap();
160        let mut wifi = data.wifi.lock().unwrap();
161        wifi.set_client_config(WifiClientConfig::default()).unwrap();
162
163        // Start WiFi
164        wifi.start().unwrap();
165
166        Ok(())
167    }
168
169    /// Checkes if device is already provisioned.
170    /// If provisioned, it returns the SSID and Password of provisioned WiFi network.
171    pub fn is_provisioned(&self) -> Option<(String, String)> {
172        let partition = self.shared.lock().unwrap().nvs_partition.clone();
173        let nvs = Nvs::new(partition, WIFI_NAMESPACE).expect("Unable to open NVS partition");
174        let mut buff = [0; 64];
175        let ssid = nvs
176            .get_string(WIFI_SSID_KEY, &mut buff)
177            .expect("Unable to get SSID");
178        let password = nvs
179            .get_string(WIFI_PASS_KEY, &mut buff)
180            .expect("Unable to get Password");
181
182        if let (Some(ssid), Some(password)) = (ssid, password) {
183            return Some((ssid, password));
184        }
185        None
186    }
187
188    fn new_with_transport(
189        wifi: WrappedInArcMutex<WifiMgr<'static>>,
190        nvs_partition: NvsPartition,
191        sec_ver: u8,
192        pop: Option<String>,
193        transport: T,
194    ) -> Result<WifiProvMgr<T>, Error> {
195        let (sender, receiver) = mpsc::channel::<()>();
196
197        let shared = wrap_in_arc_mutex(ProvisioningSharedData {
198            nvs_partition,
199            wifi,
200            scan_results: None,
201            msg_sender: sender,
202        });
203
204        Ok(WifiProvMgr {
205            sec_ver,
206            pop,
207            shared,
208            transport,
209            msg_receiver: receiver,
210        })
211    }
212
213    fn get_sec_ver_and_pop(sec: &ProtocommSecurity) -> (u8, Option<String>) {
214        let sec_ver;
215        let pop;
216
217        match &sec {
218            ProtocommSecurity::Sec0(_sec0) => {
219                sec_ver = 0;
220                pop = None;
221            }
222            ProtocommSecurity::Sec1(sec1) => {
223                sec_ver = 1;
224                pop = sec1.pop.clone();
225            }
226        }
227
228        (sec_ver, pop)
229    }
230
231    fn get_version_info(&self) -> String {
232        let mut cap = vec!["wifi_scan"];
233
234        if self.pop.is_none() {
235            cap.push("no_pop");
236        }
237
238        if self.sec_ver == 0 {
239            cap.push("no_sec");
240        }
241
242        json! ({
243            "prov": {
244                "ver": "v1.1",
245                "sec_ver": self.sec_ver,
246                "cap": cap
247            }
248        })
249        .to_string()
250    }
251
252    fn register_protocomm_endpoints(&mut self) {
253        let version_info = self.get_version_info();
254        let shared_1 = self.shared.clone();
255        let shared_2 = self.shared.clone();
256
257        self.transport.set_version_info("proto-ver", version_info);
258        self.transport.set_security_ep("prov-session");
259        self.add_endpoint(
260            "prov-scan",
261            Box::new(move |ep, data| ep_prov_scan::prov_scan_callbak(ep, data, shared_1.clone())),
262        );
263        self.add_endpoint(
264            "prov-config",
265            Box::new(move |ep, data| {
266                ep_prov_config::prov_config_callback(ep, data, shared_2.clone())
267            }),
268        );
269    }
270}
271
272mod ep_prov_scan {
273    use super::*;
274
275    use crate::proto::{
276        constants::Status,
277        wifi_prov::{
278            wifi_constants::WifiAuthMode,
279            wifi_scan::{mod_WiFiScanPayload::OneOfpayload, *},
280        },
281    };
282
283    impl From<WifiApInfo> for WiFiScanResult {
284        fn from(value: WifiApInfo) -> Self {
285            let auth = match value.auth {
286                crate::wifi::WifiAuthMode::None => WifiAuthMode::Open,
287                crate::wifi::WifiAuthMode::WEP => WifiAuthMode::WEP,
288                crate::wifi::WifiAuthMode::WPA => WifiAuthMode::WPA_PSK,
289                crate::wifi::WifiAuthMode::WPA2Personal => WifiAuthMode::WPA2_PSK,
290                crate::wifi::WifiAuthMode::WPAWPA2Personal => WifiAuthMode::WPA_WPA2_PSK,
291                crate::wifi::WifiAuthMode::WPA2Enterprise => WifiAuthMode::WPA2_ENTERPRISE,
292                crate::wifi::WifiAuthMode::WPA3Personal => WifiAuthMode::WPA3_PSK,
293                crate::wifi::WifiAuthMode::WPA2WPA3Personal => WifiAuthMode::WPA2_WPA3_PSK,
294                _ => panic!("Unknown WiFi auth type"),
295            };
296
297            Self {
298                ssid: value.ssid.into(),
299                channel: value.channel as u32,
300                bssid: value.bssid,
301                rssi: value.signal_strength as i32,
302                auth,
303            }
304        }
305    }
306
307    #[inline(always)]
308    pub fn prov_scan_callbak(
309        _ep: &str,
310        inp: &[u8],
311        shared: WrappedInArcMutex<ProvisioningSharedData>,
312    ) -> Vec<u8> {
313        let mut out_payload: Vec<u8> = Default::default();
314        let mut writer = Writer::new(&mut out_payload);
315        let mut resp = WiFiScanPayload::default();
316
317        let inp_data = match WiFiScanPayload::try_from(inp) {
318            Ok(payload) => payload,
319            Err(_) => {
320                resp.status = Status::InvalidProto;
321                resp.write_message(&mut writer).unwrap();
322                return out_payload;
323            }
324        };
325
326        let resp_msg;
327        let resp_payload = match inp_data.payload {
328            OneOfpayload::cmd_scan_start(cmd_scan_start) => {
329                resp_msg = WiFiScanMsgType::TypeRespScanStart;
330                handle_scan_start(cmd_scan_start, shared)
331            }
332            OneOfpayload::cmd_scan_status(cmd_scan_status) => {
333                resp_msg = WiFiScanMsgType::TypeRespScanStatus;
334                handle_scan_status(cmd_scan_status, shared)
335            }
336            OneOfpayload::cmd_scan_result(cmd_scan_result) => {
337                resp_msg = WiFiScanMsgType::TypeRespScanResult;
338                handle_scan_result(cmd_scan_result, shared)
339            }
340            other => {
341                log::error!("Invalid payload type {:?}", other);
342                return vec![];
343            }
344        };
345
346        resp.status = Status::Success;
347        resp.msg = resp_msg;
348        resp.payload = resp_payload;
349
350        if resp.write_message(&mut writer).is_err() {
351            log::error!("Failed to write message");
352            return vec![];
353        };
354
355        out_payload
356    }
357
358    fn handle_scan_start(
359        _cmd: CmdScanStart,
360        _shared: WrappedInArcMutex<ProvisioningSharedData>,
361    ) -> OneOfpayload {
362        let resp = RespScanStart::default();
363        OneOfpayload::resp_scan_start(resp)
364    }
365
366    fn handle_scan_status(
367        _cmd: CmdScanStatus,
368        shared: WrappedInArcMutex<ProvisioningSharedData>,
369    ) -> OneOfpayload {
370        let mut resp = RespScanStatus::default();
371
372        let mut data = shared.lock().unwrap();
373
374        let networks = data.wifi.lock().unwrap().scan().unwrap();
375
376        resp.scan_finished = true;
377        resp.result_count = networks.len() as u32;
378        log::info!("Found {} WiFi network(s)", networks.len());
379
380        data.scan_results = Some(networks);
381
382        OneOfpayload::resp_scan_status(resp)
383    }
384
385    fn handle_scan_result(
386        cmd: CmdScanResult,
387        shared: WrappedInArcMutex<ProvisioningSharedData>,
388    ) -> OneOfpayload {
389        log::info!("Sending WiFi scan results");
390
391        let mut resp = RespScanResult::default();
392
393        let data = shared.lock().unwrap();
394        let networks = data
395            .scan_results
396            .as_ref()
397            .expect("WiFi scan results not found");
398
399        let start_index = cmd.start_index as usize;
400        let count = cmd.count as usize;
401        let end_index = start_index + count;
402
403        let entries = networks
404            .clone()
405            .drain(start_index..end_index)
406            .map(|x| x.into())
407            .collect();
408
409        resp.entries = entries;
410        OneOfpayload::resp_scan_result(resp)
411    }
412}
413
414mod ep_prov_config {
415    use mod_RespGetStatus::OneOfstate;
416    use mod_WiFiConfigPayload::OneOfpayload;
417    use quick_protobuf::{MessageWrite, Writer};
418
419    use super::*;
420    use crate::{
421        proto::wifi_prov::{
422            constants::*,
423            wifi_config::*,
424            wifi_constants::{WifiConnectFailedReason, WifiConnectedState, WifiStationState},
425        },
426        wifi::WifiClientConfig,
427    };
428
429    use super::ProvisioningSharedData;
430
431    #[inline(always)]
432    pub fn prov_config_callback(
433        _ep: &str,
434        inp: &[u8],
435        shared: WrappedInArcMutex<ProvisioningSharedData>,
436    ) -> Vec<u8> {
437        let mut resp = WiFiConfigPayload::default();
438        let mut out_vec = Vec::<u8>::new();
439        let mut writer = Writer::new(&mut out_vec);
440
441        let inp_payload = WiFiConfigPayload::try_from(inp).unwrap();
442
443        let resp_payload = match inp_payload.payload {
444            mod_WiFiConfigPayload::OneOfpayload::cmd_get_status(cmd_get_status) => {
445                resp.msg = WiFiConfigMsgType::TypeRespGetStatus;
446                handle_get_status(cmd_get_status, shared)
447            }
448            mod_WiFiConfigPayload::OneOfpayload::cmd_set_config(cmd_set_config) => {
449                resp.msg = WiFiConfigMsgType::TypeRespSetConfig;
450                handle_set_config(cmd_set_config, shared)
451            }
452            mod_WiFiConfigPayload::OneOfpayload::cmd_apply_config(cmd_apply_config) => {
453                resp.msg = WiFiConfigMsgType::TypeRespApplyConfig;
454                handle_apply_config(cmd_apply_config, shared)
455            }
456            _ => unreachable!(),
457        };
458
459        resp.payload = resp_payload;
460
461        if resp.write_message(&mut writer).is_err() {
462            log::error!("Failed to write wifi_config response");
463            return vec![];
464        };
465
466        out_vec
467    }
468
469    fn handle_set_config(
470        cmd: CmdSetConfig,
471        shared: WrappedInArcMutex<ProvisioningSharedData>,
472    ) -> OneOfpayload {
473        let mut resp = RespSetConfig::default();
474
475        let ssid = String::from_utf8(cmd.ssid).expect("Failed to decode WiFi SSID");
476        let password = String::from_utf8(cmd.passphrase).expect("Failed to decode WiFi passphrase");
477        let bssid = cmd.bssid;
478        let channel = cmd.channel;
479
480        log::info!("Received SSID={} PASSWORD={}", ssid, password);
481
482        let wifi_config = WifiClientConfig {
483            ssid,
484            password,
485            bssid,
486            channel: channel as u8,
487            ..Default::default()
488        };
489
490        // SSID and Password are saved after connection so as to deal with incorrect password
491
492        let data = shared.lock().unwrap();
493        data.wifi
494            .lock()
495            .unwrap()
496            .set_client_config(wifi_config)
497            .unwrap();
498
499        resp.status = Status::Success;
500
501        OneOfpayload::resp_set_config(resp)
502    }
503
504    fn handle_apply_config(
505        _cmd: CmdApplyConfig,
506        shared: WrappedInArcMutex<ProvisioningSharedData>,
507    ) -> OneOfpayload {
508        log::info!("Connecting to WiFi");
509        let mut resp = RespApplyConfig::default();
510
511        let data = shared.lock().unwrap();
512        let mut wifi = data.wifi.lock().unwrap();
513
514        if wifi.connect().is_err() {
515            log::error!("Failed connecting to provided WiFi network");
516        } else {
517            let (client_config, _) = wifi.get_wifi_config();
518            if let Some(config) = client_config {
519                let ssid = config.ssid;
520                let password = config.password;
521                let nvs_partition = data.nvs_partition.clone();
522                let nvs = Nvs::new(nvs_partition, WIFI_NAMESPACE);
523                match nvs {
524                    Err(_) => log::error!("Failed to open nvs for saving WiFi credentials"),
525                    Ok(mut nvs) => {
526                        nvs.set_str(WIFI_SSID_KEY, &ssid)
527                            .expect("Failed to save SSID");
528                        nvs.set_str(WIFI_PASS_KEY, &password)
529                            .expect("Failed to save Password");
530                    }
531                }
532            }
533        }
534        resp.status = Status::Success;
535
536        OneOfpayload::resp_apply_config(resp)
537    }
538
539    fn handle_get_status(
540        _cmd: CmdGetStatus,
541        shared: WrappedInArcMutex<ProvisioningSharedData>,
542    ) -> OneOfpayload {
543        let mut resp = RespGetStatus::default();
544
545        let data = shared.lock().unwrap();
546
547        // TODO: send actual data
548        let wifi = data.wifi.lock().unwrap();
549        let ip_addr = wifi.get_ip_addr();
550
551        resp.status = Status::Success;
552        if wifi.is_connected() {
553            resp.sta_state = WifiStationState::Connected;
554            resp.state = OneOfstate::connected(WifiConnectedState {
555                ip4_addr: ip_addr.to_string(),
556                ..Default::default()
557            });
558        } else {
559            resp.sta_state = WifiStationState::ConnectionFailed;
560            resp.state = OneOfstate::fail_reason(WifiConnectFailedReason::AuthError);
561        }
562
563        data.msg_sender.send(()).unwrap();
564
565        OneOfpayload::resp_get_status(resp)
566    }
567}