esp-idf-matter 0.1.0

Run rs-matter on Espressif chips with ESP IDF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//! This module provides the ESP-IDF implementation of the `Netif` trait for the Matter stack, as
//! well as the `EspMatterNetStack` type alias for a STD stack which is based on `async-io` or `async-io-mini`.

use core::borrow::Borrow;
use core::future::Future;
use core::net::{Ipv4Addr, Ipv6Addr};

use embassy_sync::{self, blocking_mutex};

use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::hal::task::embassy_sync::EspRawMutex;
use esp_idf_svc::netif::EspNetif;
use esp_idf_svc::sys::EspError;

use rs_matter_stack::matter::dm::clusters::gen_diag::{InterfaceTypeEnum, NetifDiag, NetifInfo};
use rs_matter_stack::matter::dm::networks::NetChangeNotif;
use rs_matter_stack::matter::error::Error;
use rs_matter_stack::matter::utils::cell::RefCell;
use rs_matter_stack::matter::utils::sync::DynBase;

/// A network stack for ESP-IDF
pub type EspMatterNetStack = edge_nal_std::Stack;

/// A `Netif` trait implementation for ESP-IDF
pub struct EspMatterNetif<T> {
    netif_access: T,
    sysloop: EspSystemEventLoop,
    netif_type: InterfaceTypeEnum,
    netif_state: blocking_mutex::Mutex<EspRawMutex, RefCell<NetifInfoOwned>>,
}

impl<T> EspMatterNetif<T>
where
    T: EspNetifAccess,
{
    /// Create a new `EspMatterNetif` instance
    pub const fn new(
        netif_access: T,
        netif_type: InterfaceTypeEnum,
        sysloop: EspSystemEventLoop,
    ) -> Self {
        Self {
            netif_access,
            netif_type,
            sysloop,
            netif_state: blocking_mutex::Mutex::new(RefCell::new(NetifInfoOwned::new())),
        }
    }

    fn load_netif_state(&self, l2_connected: bool, netif: &EspNetif) -> Result<bool, EspError> {
        self.netif_state.lock(|state| {
            state
                .borrow_mut()
                .load(l2_connected, netif, self.netif_type)
        })
    }
}

impl<T> DynBase for EspMatterNetif<T> {}

impl<T> NetifDiag for EspMatterNetif<T> {
    fn netifs(&self, f: &mut dyn FnMut(&NetifInfo) -> Result<(), Error>) -> Result<(), Error> {
        self.netif_state.lock(|info| info.borrow().as_ref(f))
    }
}

impl<T> NetChangeNotif for EspMatterNetif<T>
where
    T: EspNetifAccess,
{
    async fn wait_changed(&self) {
        loop {
            let changed = self
                .netif_access
                .access(|netif, l2_connected| self.load_netif_state(l2_connected, netif))
                .await
                .unwrap_or(false);

            if changed {
                break;
            }

            let _ = utils::wait_any_conf_change(&self.sysloop).await;
        }
    }
}

/// A trait to abstract the way how `EspMatterNotif` gets access
/// to the `EspNetif` instance associated with the concrete network protocol (Ethernet, Wifi or Thread)
pub trait EspNetifAccess {
    /// Access the `EspNetif` instance
    ///
    /// # Arguments
    /// - `f`: A closure which is called with the `EspNetif` instance and a boolean indicating whether the underlying L2 protocol
    ///   is connected (e.g. Wifi is connected to an AP, Ethernet cable is plugged in, Thread is attached to a Thread network)
    async fn access<F, R>(&self, f: F) -> Result<R, EspError>
    where
        F: FnOnce(&EspNetif, bool) -> Result<R, EspError>;
}

impl<T> EspNetifAccess for &T
where
    T: EspNetifAccess,
{
    fn access<F, R>(&self, f: F) -> impl Future<Output = Result<R, EspError>>
    where
        F: FnOnce(&EspNetif, bool) -> Result<R, EspError>,
    {
        (*self).access(f)
    }
}

impl EspNetifAccess for &EspNetif {
    async fn access<F, R>(&self, f: F) -> Result<R, EspError>
    where
        F: FnOnce(&EspNetif, bool) -> Result<R, EspError>,
    {
        f(self.borrow(), true)
    }
}

/// A cache type for storing the information for one network interface
///
/// Necessary, because the `NetifDiag` trait is not async
#[allow(dead_code)]
#[derive(Debug)]
pub(crate) struct NetifInfoOwned {
    name: heapless::String<6>,
    operational: bool,
    hw_addr: [u8; 8],
    ipv4_addr: Ipv4Addr,
    ipv6_addr: Ipv6Addr,
    netif_type: InterfaceTypeEnum,
    netif_index: u32,
}

#[allow(dead_code)]
impl NetifInfoOwned {
    pub(crate) const fn new() -> Self {
        Self {
            name: heapless::String::new(),
            operational: false,
            hw_addr: [0; 8],
            ipv4_addr: Ipv4Addr::UNSPECIFIED,
            ipv6_addr: Ipv6Addr::UNSPECIFIED,
            netif_type: InterfaceTypeEnum::WiFi,
            netif_index: 0,
        }
    }

    pub(crate) fn is_operational(&self) -> bool {
        self.is_operational_v6() && !self.ipv4_addr.is_unspecified()
    }

    pub(crate) fn is_operational_v6(&self) -> bool {
        self.operational && !self.ipv6_addr.is_unspecified()
    }

    pub(crate) fn as_ref<F>(&self, f: F) -> Result<(), Error>
    where
        F: FnOnce(&NetifInfo<'_>) -> Result<(), Error>,
    {
        let ipv4_addrs = [self.ipv4_addr];
        let ipv6_addrs = [self.ipv6_addr];

        f(&NetifInfo {
            name: &self.name,
            operational: self.operational,
            hw_addr: &self.hw_addr,
            ipv4_addrs: if self.ipv4_addr.is_unspecified() {
                &[]
            } else {
                &ipv4_addrs
            },
            ipv6_addrs: if self.ipv6_addr.is_unspecified() {
                &[]
            } else {
                &ipv6_addrs
            },
            netif_type: self.netif_type,
            offprem_svc_reachable_ipv4: None,
            offprem_svc_reachable_ipv6: None,
            netif_index: self.netif_index,
        })
    }

    pub(crate) fn load(
        &mut self,
        l2_connected: bool,
        netif: &EspNetif,
        netif_type: InterfaceTypeEnum,
    ) -> Result<bool, EspError> {
        utils::get_netif_conf(netif, netif_type, |info| {
            Ok(self.load_from_info(l2_connected, info))
        })
    }

    fn load_from_info(&mut self, l2_connected: bool, info: &NetifInfo<'_>) -> bool {
        let hw_addr: &[u8] = info.hw_addr;

        let ipv4_addr = utils::info_ipv4_addr(info);
        let ipv6_addr = utils::info_ipv6_addr(info);

        // An interface counts as operational only if the L2 protocol is connected too.
        // (An ESP netif stays "up" while - say - Thread is detached or Wifi is
        // disconnected, so `info.operational` alone is not enough.)
        let operational = info.operational && l2_connected;

        let changed = self.name != info.name
            || self.operational != operational
            || self.hw_addr != hw_addr
            || self.ipv4_addr != ipv4_addr
            || self.ipv6_addr != ipv6_addr
            || self.netif_type != info.netif_type
            || self.netif_index != info.netif_index;

        if changed {
            self.name = info.name.try_into().unwrap();
            self.operational = operational;
            self.hw_addr = hw_addr.try_into().unwrap();
            self.ipv4_addr = ipv4_addr;
            self.ipv6_addr = ipv6_addr;
            self.netif_type = info.netif_type;
            self.netif_index = info.netif_index;
        }

        changed
    }
}

/// Utility functions for working with the ESP-IDF `EspNetif` type
pub mod utils {
    use core::net::{Ipv4Addr, Ipv6Addr};
    use core::pin::pin;

    use alloc::sync::Arc;

    use embassy_futures::select::select;
    use embassy_time::{Duration, Timer};

    // This `Notification` is one of rs-matter's, hence parameterized with a
    // `RawMutex` from embassy-sync 0.8 (rather than the 0.7 `EspRawMutex`).
    use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

    use esp_idf_svc::eventloop::EspSystemEventLoop;
    use esp_idf_svc::handle::RawHandle;
    use esp_idf_svc::netif::{EspNetif, IpEvent};
    use esp_idf_svc::sys::{
        esp_ip6_addr_t, esp_netif_get_all_ip6, esp_netif_get_all_preferred_ip6, EspError,
        LWIP_IPV6_NUM_ADDRESSES,
    };

    use rs_matter_stack::matter::dm::clusters::gen_diag::{InterfaceTypeEnum, NetifInfo};
    use rs_matter_stack::matter::utils::sync::Notification;

    extern crate alloc;

    /// Get the network interface configuration as a `NetifInfo` structure
    pub fn get_netif_conf<F, R>(
        netif: &EspNetif,
        netif_type: InterfaceTypeEnum,
        f: F,
    ) -> Result<R, EspError>
    where
        F: FnOnce(&NetifInfo) -> Result<R, EspError>,
    {
        let ip_info = netif.get_ip_info()?;

        let ipv4: Ipv4Addr = ip_info.ip.octets().into();

        // Collect *all* of the netif's IPv6 addresses, the preferred ones first.
        //
        // We must not keep only one (e.g. the last) here: the downstream consumer
        // (`info_ipv6_addr`) filters this list, and on Wifi the netif typically has both
        // a link-local and a global/ULA SLAAC address. If only a single address were
        // passed and it happened to be the global one, the link-local filter would find
        // nothing and the interface would never be reported operational.
        //
        // Ordering the preferred addresses first is what lets `info_ipv6_addr` single out
        // a routable address on Thread. ESP-IDF's OpenThread netif glue deliberately marks
        // link-local and mesh-local addresses as deprecated and everything else (i.e. the
        // OMR address a border router hands out) as preferred, so "preferred" is exactly
        // the "reachable from off-mesh" set - the same set OpenThread itself registers as
        // the SRP host's AAAA records.
        let ipv6_addrs = {
            let mut raw: [esp_ip6_addr_t; LWIP_IPV6_NUM_ADDRESSES as usize] = Default::default();

            let mut ipv6_addrs =
                heapless::Vec::<Ipv6Addr, { LWIP_IPV6_NUM_ADDRESSES as usize }>::new();

            let count =
                unsafe { esp_netif_get_all_preferred_ip6(netif.handle() as _, raw.as_mut_ptr()) };

            for raw in &raw[..count as usize] {
                // `unwrap` cannot fail: `count <= LWIP_IPV6_NUM_ADDRESSES`
                ipv6_addrs.push(esp_ip6_to_addr(raw)).unwrap();
            }

            let count = unsafe { esp_netif_get_all_ip6(netif.handle() as _, raw.as_mut_ptr()) };

            for raw in &raw[..count as usize] {
                let addr = esp_ip6_to_addr(raw);

                if !ipv6_addrs.contains(&addr) {
                    // `unwrap` cannot fail: the preferred addresses are a subset of all of them
                    ipv6_addrs.push(addr).unwrap();
                }
            }

            ipv6_addrs
        };

        let mut mac: [u8; 8] = Default::default();
        mac[..6].copy_from_slice(&netif.get_mac()?);

        f(&NetifInfo {
            name: &netif.get_name(),
            operational: if matches!(netif_type, InterfaceTypeEnum::Thread) {
                netif.is_netif_up()?
            } else {
                netif.is_up()?
            },
            offprem_svc_reachable_ipv4: None,
            offprem_svc_reachable_ipv6: None,
            hw_addr: &mac,
            ipv4_addrs: &[ipv4],
            ipv6_addrs: &ipv6_addrs,
            netif_type,
            netif_index: netif.get_index(),
        })
    }

    /// Convert a raw lwIP `esp_ip6_addr_t` into a `core::net::Ipv6Addr`
    fn esp_ip6_to_addr(raw: &esp_ip6_addr_t) -> Ipv6Addr {
        let mut octets = [0u8; 16];

        for (i, word) in raw.addr.iter().enumerate() {
            octets[i * 4..i * 4 + 4].copy_from_slice(&word.to_le_bytes());
        }

        octets.into()
    }

    pub fn info_is_operational(l2_connected: bool, info: &NetifInfo<'_>) -> bool {
        info_is_operational_v6(l2_connected, info) && !info_ipv4_addr(info).is_unspecified()
    }

    pub fn info_is_operational_v6(l2_connected: bool, info: &NetifInfo<'_>) -> bool {
        l2_connected && info.operational && !info_ipv6_addr(info).is_unspecified()
    }

    /// Wait for any IP configuration change
    pub async fn wait_any_conf_change(sysloop: &EspSystemEventLoop) -> Result<(), EspError> {
        const TIMEOUT_PERIOD_SECS: u8 = 5;

        let notification = Arc::new(Notification::<CriticalSectionRawMutex>::new());

        let _subscription = {
            let notification = notification.clone();

            sysloop.subscribe::<IpEvent, _>(move |_| {
                notification.notify();
            })
        }?;

        let mut events = pin!(notification.wait());
        let mut timer = pin!(Timer::after(Duration::from_secs(TIMEOUT_PERIOD_SECS as _)));

        select(&mut events, &mut timer).await;

        Ok(())
    }

    pub(crate) fn info_ipv4_addr(info: &NetifInfo<'_>) -> Ipv4Addr {
        info.ipv4_addrs
            .first()
            .copied()
            .unwrap_or(Ipv4Addr::UNSPECIFIED)
    }

    pub(crate) fn info_ipv6_addr(info: &NetifInfo<'_>) -> Ipv6Addr {
        let ipv6_addr = if matches!(info.netif_type, InterfaceTypeEnum::Thread) {
            // For Thread: the first non-link-local address. Since `get_netif_conf` orders
            // the preferred addresses first, this is the OMR address whenever a border
            // router has advertised an on-mesh prefix, and the mesh-local EID otherwise.
            //
            // Neither the link-local nor the mesh-local address is reachable from outside
            // the Thread mesh, so neither is a meaningful thing to report as "the" address
            // of the interface. Beyond that, the Matter stack keys the (re)start of the
            // mDNS publisher off the netif state this address is part of, and both of the
            // other two are poor change signals: the link-local address is derived from the
            // IEEE 802.15.4 extended address and so is identical across Thread networks,
            // while latching onto the mesh-local EID would hide the moment the routable OMR
            // address appears - which is exactly when the SRP records want re-publishing.
            info.ipv6_addrs
                .iter()
                .find(|ipv6| !ipv6.is_unicast_link_local())
                .or_else(|| info.ipv6_addrs.first())
        } else {
            // For Wifi: locate the link-local Ipv6 address
            info.ipv6_addrs
                .iter()
                .find(|ipv6| ipv6.is_unicast_link_local())
        };

        ipv6_addr.copied().unwrap_or(Ipv6Addr::UNSPECIFIED)
    }
}