ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
mdns_adaptive.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file mdns_adaptive.cpp
6 * @brief Adaptive mDNS beacon scheduling (see mdns_adaptive.h).
7 */
8
10
11#if PC_ENABLE_MDNS_ADAPTIVE
12
13#if defined(ARDUINO) && PC_ENABLE_MDNS && PC_ENABLE_PROMISC
14#include "network_drivers/physical/physical.h" // pc_net_channel
15#include "services/net/mdns_service/mdns_service.h" // pc_mdns_txt
16#include "services/radio/promisc/promisc.h" // pc_promisc_*
17#include "services/system/clock.h" // pc_millis
18#endif
19uint32_t pc_mdns_refresh_interval(uint32_t ttl_s)
20{
21 // Half the TTL, in ms; guard the *1000 against overflow.
22 uint64_t half_ms = (uint64_t)ttl_s * 1000 / 2;
23 return half_ms > 0xFFFFFFFFu ? 0xFFFFFFFFu : (uint32_t)half_ms;
24}
25
26void pc_mdns_beacon_init(MdnsBeacon *b, uint32_t base_ms, uint32_t max_ms, uint16_t hi_thresh)
27{
28 if (!b)
29 {
30 return;
31 }
32 b->base_ms = base_ms;
33 b->max_ms = max_ms < base_ms ? base_ms : max_ms;
34 b->cur_ms = base_ms;
35 b->hi_thresh = hi_thresh ? hi_thresh : 1;
36}
37
38uint32_t pc_mdns_beacon_adapt(MdnsBeacon *b, uint16_t contention)
39{
40 if (!b)
41 {
42 return 0;
43 }
44 if (contention >= b->hi_thresh)
45 {
46 uint32_t up = b->cur_ms << 1;
47 if (up < b->cur_ms || up > b->max_ms) // overflow or past ceiling
48 {
49 up = b->max_ms;
50 }
51 b->cur_ms = up;
52 }
53 else if (contention == 0)
54 {
55 uint32_t down = b->cur_ms >> 1;
56 if (down < b->base_ms)
57 {
58 down = b->base_ms;
59 }
60 b->cur_ms = down;
61 }
62 return b->cur_ms;
63}
64
65bool pc_mdns_beacon_due(const MdnsBeacon *b, uint32_t last_ms, uint32_t now_ms)
66{
67 if (!b)
68 {
69 return false;
70 }
71 uint32_t elapsed = now_ms - last_ms; // wrap-safe modular subtraction
72 return elapsed >= b->cur_ms;
73}
74
75bool pc_mdns_beacon_presleep_due(const MdnsBeacon *b, uint32_t last_ms, uint32_t now_ms, uint32_t sleep_ms)
76{
77 if (!b)
78 {
79 return false;
80 }
81 uint32_t elapsed = now_ms - last_ms;
82 // Would the record lapse during the sleep? Compute in 64-bit so elapsed + sleep_ms cannot wrap.
83 uint64_t after = (uint64_t)elapsed + sleep_ms;
84 return after >= b->cur_ms;
85}
86
87// ---------------------------------------------------------------------------
88// Contention sampling
89// ---------------------------------------------------------------------------
90
91void pc_mdns_contention_init(MdnsContentionWindow *w, uint32_t window_ms, uint32_t frames_now, uint32_t now_ms)
92{
93 if (!w)
94 {
95 return;
96 }
97 w->last_count = frames_now;
98 w->last_ms = now_ms;
99 w->window_ms = window_ms ? window_ms : 1000;
100}
101
102bool pc_mdns_contention_sample(MdnsContentionWindow *w, uint32_t frames_now, uint32_t now_ms, uint16_t *out)
103{
104 if (!w || !out)
105 {
106 return false;
107 }
108 uint32_t elapsed = now_ms - w->last_ms; // wrap-safe modular subtraction
109 if (elapsed < w->window_ms)
110 {
111 return false;
112 }
113 // Modular difference, so a wrapped frame counter still yields the true count as long as fewer
114 // than 2^32 frames passed in one window - which no radio does in a second.
115 uint32_t delta = frames_now - w->last_count;
116 *out = delta > 0xFFFF ? 0xFFFF : (uint16_t)delta;
117 w->last_count = frames_now;
118 w->last_ms = now_ms;
119 return true;
120}
121
122// ---------------------------------------------------------------------------
123// Device binding
124// ---------------------------------------------------------------------------
125
126#if defined(ARDUINO) && PC_ENABLE_MDNS && PC_ENABLE_PROMISC
127
128/** @brief Owned state for the live adaptive announcer. */
129struct MdnsAdaptiveCtx
130{
131 MdnsAdaptiveCfg cfg;
132 MdnsBeacon beacon;
133 MdnsContentionWindow window;
134 volatile uint32_t frames; ///< running frame total, bumped in the capture callback.
135 uint32_t last_announce_ms;
136 uint16_t last_contention;
137 uint32_t announces;
138 uint8_t channel; ///< the channel capture is currently pinned to.
139 bool running;
140};
141static MdnsAdaptiveCtx s_ad = {};
142
143// Promiscuous sink: the whole job is to count. Runs in the WiFi driver's callback context, so it
144// only touches the running total - no parsing, no allocation, no blocking.
145static void adaptive_sink(const uint8_t *frame, uint16_t len, int8_t rssi, uint8_t channel)
146{
147 (void)frame;
148 (void)len;
149 (void)rssi;
150 (void)channel;
151 s_ad.frames++;
152}
153
154bool pc_mdns_adaptive_begin(const MdnsAdaptiveCfg *cfg)
155{
156 if (!cfg || s_ad.running)
157 {
158 return false;
159 }
160 uint8_t ch = pc_net_channel();
161 if (ch == 0)
162 {
163 return false; // not associated: there is no channel to pin capture to
164 }
165
166 s_ad.cfg = *cfg;
167 uint32_t now = pc_millis();
168 uint32_t base = pc_mdns_refresh_interval(cfg->ttl_s);
169
170 // Never let the backoff push the refresh past the TTL: a cache evicts the record at its TTL, so
171 // announcing slower than that makes the device silently undiscoverable - the opposite of the
172 // point. Cap the ceiling at 7/8 of the TTL, leaving margin for propagation. A longer TTL is how
173 // you buy a wider adaptive range; the range is fundamentally bounded by [TTL/2, ~TTL).
174 uint64_t ttl_ms = (uint64_t)cfg->ttl_s * 1000;
175 uint32_t safe_ceiling = (uint32_t)(ttl_ms - ttl_ms / 8 > 0xFFFFFFFFu ? 0xFFFFFFFFu : ttl_ms - ttl_ms / 8);
176 uint32_t ceiling = cfg->max_interval_ms < safe_ceiling ? cfg->max_interval_ms : safe_ceiling;
177 pc_mdns_beacon_init(&s_ad.beacon, base, ceiling, cfg->hi_contention);
178 s_ad.frames = 0;
179 pc_mdns_contention_init(&s_ad.window, cfg->window_ms, 0, now);
180 s_ad.last_announce_ms = now;
181 s_ad.last_contention = 0;
182 s_ad.announces = 0;
183 s_ad.channel = ch;
184
185 // Pin capture to the station's OWN channel and never hop, or the association drops.
186 s_ad.running = pc_promisc_begin(ch, adaptive_sink);
187 return s_ad.running;
188}
189
190void pc_mdns_adaptive_tick(void)
191{
192 if (!s_ad.running)
193 {
194 return;
195 }
196 uint32_t now = pc_millis();
197
198 // Follow the station if it roamed to another channel, so capture stays on the live link.
199 uint8_t ch = pc_net_channel();
200 if (ch != 0 && ch != s_ad.channel)
201 {
202 pc_promisc_set_channel(ch);
203 s_ad.channel = ch;
204 }
205
206 // Close a contention window if one elapsed, and let it move the interval.
207 uint16_t c;
208 if (pc_mdns_contention_sample(&s_ad.window, s_ad.frames, now, &c))
209 {
210 s_ad.last_contention = c;
211 pc_mdns_beacon_adapt(&s_ad.beacon, c);
212 }
213
214 // Re-announce when the (adaptive) interval has elapsed. Re-applying the TXT at its current value
215 // re-announces on every PCB with no goodbye - a refresh, not an evict.
216 if (pc_mdns_beacon_due(&s_ad.beacon, s_ad.last_announce_ms, now))
217 {
218 pc_mdns_txt(s_ad.cfg.key, s_ad.cfg.value);
219 s_ad.last_announce_ms = now;
220 s_ad.announces++;
221 }
222}
223
224void pc_mdns_adaptive_end(void)
225{
226 if (!s_ad.running)
227 {
228 return;
229 }
230 pc_promisc_end();
231 s_ad.running = false;
232}
233
234uint32_t pc_mdns_adaptive_interval_ms(void)
235{
236 return s_ad.beacon.cur_ms;
237}
238
239uint16_t pc_mdns_adaptive_contention(void)
240{
241 return s_ad.last_contention;
242}
243
244uint32_t pc_mdns_adaptive_announces(void)
245{
246 return s_ad.announces;
247}
248
249#endif // ARDUINO && PC_ENABLE_MDNS && PC_ENABLE_PROMISC
250
251#endif // PC_ENABLE_MDNS_ADAPTIVE
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
Adaptive mDNS beacon scheduling: RF-aware backoff, TTL refresher, auto-sleep beacon (PC_ENABLE_MDNS_A...
Optional mDNS / DNS-SD advertisement (PC_ENABLE_MDNS).
bool pc_mdns_txt(const char *key, const char *value)
Add a TXT key/value record to the advertised _http._tcp service.
uint8_t pc_net_channel(void)
Station WiFi channel (1..14), or 0 if not associated (and on host builds).
Definition physical.cpp:134
Layer 1 (Physical) - link bring-up and live egress-interface reporting.
Wi-Fi promiscuous (monitor) capture (PC_ENABLE_PROMISC) - passive 802.11 sniffing.