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
//! ESP-NOW transport for the HT emitter.
//!
//! An emitter has to put known RF energy into the channel without associating. Raw injection
//! (`esp_wifi_80211_tx`) is one way to do that and it works on the newer MACs, but on the classic
//! parts it does not radiate: measured on an ESP32-S3, the driver returns `ESP_OK` for every
//! frame, the forced-TX call reports `rc = 0` before and after start, the emitter logs
//! "Emitter running" — and three independent collectors see nothing, while the same board beacons
//! normally as a softAP. That was reproduced against four different configurations (the AX-era
//! struct API, the legacy per-interface rate API, a management probe-request instead of a data
//! frame, and no forced rate at all), so it is not a rate or frame-type problem.
//!
//! ESP-NOW does not have that problem, and never did — it is how this crate transmitted from
//! classic chips before the emitter/collector rework. It is connectionless in exactly the way an
//! emitter needs (no association, no handshake, no reply expected) and it reaches the air through
//! the MAC's ordinary vendor-action-frame path rather than the raw-TX hook.
//!
//! **There is no ping-pong here.** This module only *sends*: a broadcast peer, a fixed payload, one
//! frame per period, nothing received and nothing awaited. A collector sees these frames
//! promiscuously like any other traffic and derives CSI from the PPDU preamble, so nothing on the
//! receive side needs to know ESP-NOW is involved.
//!
//! **Scope: classic MACs only.** The C5/C6 keep raw injection — measured working there (480/s) —
//! and so does HE20, which is C5/C6-only and lives in the proprietary crate. Nothing here touches
//! [`super::frame`], whose `build_probe_frame` / `inject_probe_once` the HE20 injector imports from
//! this crate, so the HE20 path is unaffected by this module.
use ;
use WifiController;
use cratelog_ln;
/// Broadcast peer address — the emitter's default destination.
pub const BROADCAST: = ;
/// `wifi_phy_mode_t` values used for per-peer PHY forcing.
const WIFI_PHY_MODE_HT20: u32 = 4;
const WIFI_PHY_MODE_HT40: u32 = 5;
/// `WIFI_PHY_RATE_MCS0_LGI` — the slowest HT rate, and the most robust.
const WIFI_PHY_RATE_MCS0_LGI: u32 = 16;
/// Mirror of ESP-IDF's `esp_now_rate_config_t` / `wifi_tx_rate_config_t`.
unsafe extern "C"
/// Force a peer's ESP-NOW frames to HT20 or HT40 at MCS0 long-GI.
///
/// Returns the driver status; non-zero means the frames go out at whatever rate ESP-NOW picks,
/// which is a legacy rate. Surfaced rather than discarded, because an unforced PHY looks identical
/// to a working emitter until the receiver is inspected.
/// Bring the STA interface up and register the destination as an ESP-NOW peer with a forced HT PHY.
///
/// The interface must be *started* before any of this: per-peer rate config is silently ignored on
/// a stopped interface, and `add_peer` rejects a Station-interface peer outright.
/// Hand one broadcast sounding frame to the driver without waiting for it to finish.
///
/// The payload is fixed and meaningless — CSI comes from the PPDU preamble, so only the frame's
/// existence and its PHY matter.
///
/// This polls the send future **exactly once**, which is neither of the two obvious options:
///
/// - *Dropping it unpolled* transmits nothing: `send_async` only calls `esp_now_send` on its first
/// poll, so an unpolled future never queues the frame (measured: 1 frame in 8 s at 2 ms).
/// - *Awaiting it* caps the rate at ~55/s, because it waits for the TX-done callback, which takes
/// ~18 ms. That is scheduling latency, not airtime — a 32-byte MCS0 frame is microseconds — so
/// awaiting throws away most of the achievable rate.
///
/// Polling once runs the enqueue path *with* esp-radio's bookkeeping (it clears the callback flag
/// and registers the waker before calling `esp_now_send`) and then abandons the wait. Calling the C
/// `esp_now_send` symbol directly instead skips that bookkeeping: it returns `rc = 0` and radiates
/// nothing, which is why this goes through the future rather than the raw symbol.
///
/// `Pending` after that first poll is the success case: the frame is queued and in flight.