Skip to main content

hick_embassy/
mdns.rs

1//! Shared mDNS state + the handle API for embassy.
2
3use alloc::vec::Vec;
4use core::cell::RefCell;
5
6use embassy_futures::select::{select, select3};
7use embassy_net::{IpCidr, udp::UdpSocket};
8use embassy_sync::{blocking_mutex::raw::NoopRawMutex, signal::Signal};
9use embassy_time::{Instant, Timer};
10use hick_smoltcp::Engine;
11use mdns_proto::{
12  CollectedAnswer, EndpointConfig, QueryHandle, QuerySpec, ServiceHandle, ServiceSpec,
13  error::{RegisterServiceError, StartQueryError},
14  event::{EndpointEvent, QueryUpdate, ServiceUpdate},
15};
16use rand_core::Rng;
17
18use crate::{
19  io::{DualUdp, wait_either_recv},
20  time::EmbassyInstant,
21};
22
23/// Shared mDNS state: the engine behind a `RefCell`, plus a wake signal.
24///
25/// Share one instance between the driver task ([`Self::run`]) and the
26/// application — via `Rc` or a `&'static` (`StaticCell`). Every method takes
27/// `&self` (interior mutability). This is a single-executor (`!Send`) design:
28/// the driver and the handle calls cooperate on one executor, so a `RefCell`
29/// borrow never spans an `.await`.
30pub struct MdnsState<R> {
31  engine: RefCell<Engine<EmbassyInstant, R>>,
32  wake: Signal<NoopRawMutex, ()>,
33}
34
35impl<R: Rng> MdnsState<R> {
36  /// Create the shared state from a proto-layer config and an RNG (used for
37  /// probe-tiebreak seeds and query transaction ids).
38  pub fn new(config: EndpointConfig, rng: R) -> Self {
39    Self {
40      engine: RefCell::new(Engine::new(config, rng)),
41      wake: Signal::new(),
42    }
43  }
44
45  /// Set the device's local subnets — the RFC 6762 §11 on-link heuristic used when
46  /// the transport can't surface the received hop-limit (embassy-net re-exports
47  /// smoltcp's `UdpMetadata`, which carries no RX TTL).
48  ///
49  /// OPTIONAL. With no subnets configured the §11 gate accepts every inbound mDNS
50  /// datagram (the groups are link-scoped multicast routers do not forward) rather
51  /// than dropping all of it and going deaf. Configure the device's own subnets to
52  /// additionally REJECT sources outside them.
53  pub fn set_local_subnets(&self, subnets: Vec<IpCidr>) {
54    self.engine.borrow_mut().set_local_subnets(subnets);
55  }
56
57  /// Register a service; the driver starts probing/announcing it promptly.
58  pub fn register_service(&self, spec: ServiceSpec) -> Result<ServiceHandle, RegisterServiceError> {
59    let handle = self.engine.borrow_mut().register_service(spec, now())?;
60    self.wake.signal(());
61    Ok(handle)
62  }
63
64  /// Unregister a service and release its route slot.
65  pub fn unregister_service(&self, handle: ServiceHandle) {
66    self.engine.borrow_mut().unregister_service(handle, now());
67    self.wake.signal(());
68  }
69
70  /// Start a query / DNS-SD browse.
71  pub fn start_query(&self, spec: QuerySpec) -> Result<QueryHandle, StartQueryError> {
72    let handle = self.engine.borrow_mut().start_query(spec, now())?;
73    self.wake.signal(());
74    Ok(handle)
75  }
76
77  /// Cancel a query and free its slot.
78  pub fn cancel_query(&self, handle: QueryHandle) {
79    self.engine.borrow_mut().cancel_query(handle);
80    self.wake.signal(());
81  }
82
83  /// Snapshot the answers a query has collected so far — the browse / discovery
84  /// results — into an owned `Vec`. Empty if `handle` is not an active query. An
85  /// OWNED snapshot (not a borrow) because the engine lives behind a `RefCell`
86  /// whose guard cannot escape this call. Compare its length against
87  /// [`Self::query_accepted_count`] to detect answers the `max_answers` cap evicted
88  /// before you read them.
89  pub fn collected_answers(&self, handle: QueryHandle) -> Vec<CollectedAnswer> {
90    self
91      .engine
92      .borrow()
93      .collected_answers(handle)
94      .cloned()
95      .collect()
96  }
97
98  /// Total answers ever accepted by a query (including ones the `max_answers` cap
99  /// has since evicted from [`Self::collected_answers`]). `None` if `handle` is not
100  /// an active query.
101  pub fn query_accepted_count(&self, handle: QueryHandle) -> Option<u64> {
102    self.engine.borrow().query_accepted_count(handle)
103  }
104
105  /// Pop one pending update for a registered service.
106  pub fn poll_service_update(&self, handle: ServiceHandle) -> Option<ServiceUpdate> {
107    self.engine.borrow_mut().poll_service_update(handle)
108  }
109
110  /// Pop one pending update for a query.
111  pub fn poll_query_update(&self, handle: QueryHandle) -> Option<QueryUpdate> {
112    self.engine.borrow_mut().poll_query_update(handle)
113  }
114
115  /// Pop one endpoint-level event.
116  pub fn poll_endpoint_event(&self) -> Option<EndpointEvent> {
117    self.engine.borrow_mut().poll_endpoint_event()
118  }
119
120  /// Take a consistent point-in-time snapshot of every I/O counter and gauge.
121  #[cfg(feature = "stats")]
122  #[cfg_attr(docsrs, doc(cfg(feature = "stats")))]
123  pub fn stats(&self) -> hick_trace::stats::StatsSnapshot {
124    self.engine.borrow().stats()
125  }
126
127  /// Run the mDNS driver loop over the given v4 and/or v6 sockets until the task
128  /// is dropped. Never returns; spawn it as an embassy task.
129  ///
130  /// Each socket must already be bound to port 5353 and joined to the relevant
131  /// mDNS group (`224.0.0.251` / `ff02::fb`) via `Stack::join_multicast_group`.
132  /// Takes the sockets by `&mut` so it can ENFORCE the RFC 6762 §11 egress hop-limit
133  /// (255) once at startup — see [`crate::run`].
134  pub async fn run(
135    &self,
136    mut v4: Option<&mut UdpSocket<'_>>,
137    mut v6: Option<&mut UdpSocket<'_>>,
138    scratch: &mut [u8],
139  ) -> ! {
140    // RFC 6762 §11: force hop-limit 255 on egress while we still hold `&mut`.
141    if let Some(s) = v4.as_deref_mut() {
142      s.set_hop_limit(Some(255));
143      #[cfg(feature = "defmt")]
144      defmt::debug!("hick-embassy MdnsState: v4 hop-limit set to 255 (RFC 6762 §11)");
145    }
146    if let Some(s) = v6.as_deref_mut() {
147      s.set_hop_limit(Some(255));
148      #[cfg(feature = "defmt")]
149      defmt::debug!("hick-embassy MdnsState: v6 hop-limit set to 255 (RFC 6762 §11)");
150    }
151    loop {
152      let deadline = {
153        let mut engine = self.engine.borrow_mut();
154        let mut io = DualUdp::new(v4.as_deref(), v6.as_deref());
155        engine.pump(now(), &mut io, scratch)
156      };
157      // Wake when: a datagram arrives on either socket, the next protocol
158      // deadline elapses, or a handle signals new work.
159      match deadline {
160        Some(d) => {
161          #[cfg(feature = "defmt")]
162          defmt::trace!("hick-embassy MdnsState: waiting for recv, deadline, or wake signal");
163          let _ = select3(
164            wait_either_recv(v4.as_deref(), v6.as_deref()),
165            Timer::at(d.0),
166            self.wake.wait(),
167          )
168          .await;
169        }
170        None => {
171          #[cfg(feature = "defmt")]
172          defmt::trace!("hick-embassy MdnsState: no deadline, waiting for recv or wake signal");
173          let _ = select(
174            wait_either_recv(v4.as_deref(), v6.as_deref()),
175            self.wake.wait(),
176          )
177          .await;
178        }
179      }
180    }
181  }
182}
183
184/// The current monotonic instant, wrapped for the proto.
185fn now() -> EmbassyInstant {
186  EmbassyInstant(Instant::now())
187}
188
189#[cfg(test)]
190#[allow(clippy::unwrap_used, clippy::expect_used)]
191mod tests;