first
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
// tdma — burst-level bandwidth TDMA demo (see docs/narrowband.md).
|
||||
//
|
||||
// One binary, one adapter, one role (compose scenarios by running instances):
|
||||
// DEVOURER_TDMA_ROLE=tx alternate narrowband/wide BURSTS, injecting
|
||||
// critical frames in the narrowband burst and
|
||||
// bulk frames in the wide burst, flipping
|
||||
// bandwidth with FastSetBandwidth.
|
||||
// DEVOURER_TDMA_ROLE=rx-sync switch bandwidth in LOCKSTEP with the TX,
|
||||
// DEVOURER_TDMA_SYNC=wallclock (shared system
|
||||
// clock) or =marker (self-clock off the TX's
|
||||
// per-burst marker frame — no shared clock).
|
||||
// DEVOURER_TDMA_ROLE=rx-camp camp permanently on one width
|
||||
// (DEVOURER_TDMA_CAMP) — mode 2's per-band RX.
|
||||
//
|
||||
// See tdma.h for the schedule math, the on-air TD frame tag, and the full env
|
||||
// knob list. Metrics are JSONL on stdout ({"ev":"tdma.*"}).
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#include <libusb.h>
|
||||
#elif defined(__MINGW32__) || defined(__MINGW64__)
|
||||
#include <libusb-1.0/libusb.h>
|
||||
#elif defined(__APPLE__) || defined(__ANDROID__)
|
||||
#include <libusb.h>
|
||||
#else
|
||||
#include <libusb-1.0/libusb.h>
|
||||
#endif
|
||||
|
||||
// Portable sleeps (no <unistd.h> — MSVC lacks it): std::this_thread only.
|
||||
static inline void sleep_us(long us) {
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(us));
|
||||
}
|
||||
|
||||
#include "DeviceSession.h"
|
||||
#include "RadiotapBuilder.h"
|
||||
#include "RxPacket.h"
|
||||
#include "SignalStop.h"
|
||||
#include "UsbOpen.h"
|
||||
#include "WiFiDriver.h"
|
||||
#include "env_config.h"
|
||||
#include "logger.h"
|
||||
#include "tdma.h"
|
||||
|
||||
#define USB_VENDOR_ID 0x0bda
|
||||
static constexpr uint16_t kRealtekProductIds[] = {
|
||||
0x8812, 0x0811, 0xa811, 0xb811, 0x8813,
|
||||
};
|
||||
|
||||
// --- shared RX state (control thread switches, RX thread counts) ------------
|
||||
static std::atomic<int> g_rx_mhz{20}; // RX's current width
|
||||
static std::atomic<uint64_t> g_cnt[2][3]; // [nb?][class]
|
||||
static std::atomic<int64_t> g_marker_anchor_ns{0}; // steady ns of last marker
|
||||
static std::atomic<bool> g_have_anchor{false};
|
||||
// TSF-sync state: the host↔hardware-TSF fit, the reconstructed tsf of the last
|
||||
// marker (the anchor), and the measured TX↔RX crystal drift.
|
||||
static bool g_tsf_mode = false;
|
||||
static tdma::TsfClock g_clock;
|
||||
static std::mutex g_clock_mu;
|
||||
static std::atomic<int64_t> g_marker_tsf{0};
|
||||
static std::atomic<double> g_drift_ppm{1e9}; // 1e9 = not yet measured
|
||||
static uint64_t g_prev_tx_tsf = 0;
|
||||
static int64_t g_prev_rx_tsf = 0;
|
||||
|
||||
static int64_t steady_ns() {
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
static void emit(const char* json) { std::fputs(json, stdout); std::fflush(stdout); }
|
||||
|
||||
// --- device open (mirrors examples/svctx/main.cpp) --------------------------
|
||||
static libusb_device_handle* open_device(
|
||||
const std::shared_ptr<Logger>& logger, libusb_context** ctx,
|
||||
std::shared_ptr<devourer::UsbDeviceLock>& lock) {
|
||||
if (libusb_init(ctx) < 0) return nullptr;
|
||||
libusb_set_option(*ctx, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_WARNING);
|
||||
uint16_t vid = USB_VENDOR_ID, pid = 0;
|
||||
if (const char* v = std::getenv("DEVOURER_VID")) vid = (uint16_t)strtoul(v, 0, 0);
|
||||
if (const char* p = std::getenv("DEVOURER_PID")) pid = (uint16_t)strtoul(p, 0, 0);
|
||||
libusb_device_handle* h = nullptr;
|
||||
for (uint16_t p : kRealtekProductIds) {
|
||||
if (pid != 0 && p != pid) continue;
|
||||
h = libusb_open_device_with_vid_pid(*ctx, vid, p);
|
||||
if (h) break;
|
||||
}
|
||||
if (!h && pid != 0) h = libusb_open_device_with_vid_pid(*ctx, vid, pid);
|
||||
if (!h) { logger->error("no device {:04x}:{:04x}", vid, pid); return nullptr; }
|
||||
if (devourer::claim_interface_then_reset(h, devourer::find_wifi_interface(h), logger, std::getenv("DEVOURER_SKIP_RESET") == nullptr, lock) != 0) {
|
||||
logger->error("claim failed (busy?)");
|
||||
return nullptr;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// --- TX role ----------------------------------------------------------------
|
||||
static void run_tx(IRadio* dev, const tdma::Config& c) {
|
||||
dev->InitWrite(SelectedChannel{c.channel, 0, CHANNEL_WIDTH_20});
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
const auto rt_crit = devourer::build_stream_radiotap(c.crit_rate);
|
||||
const auto rt_bulk = devourer::build_stream_radiotap(c.bulk_rate);
|
||||
// Markers ride the robust critical rate (they must be heard to sync).
|
||||
const auto& rt_marker = rt_crit;
|
||||
|
||||
ChannelWidth_t cur_w = CHANNEL_WIDTH_20;
|
||||
int64_t last_marker_burst = -1;
|
||||
uint32_t seq[3] = {0, 0, 0};
|
||||
auto next_stat = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
fprintf(stderr, "tdma tx: nb=%dMHz/%dms wide=%dMHz/%dms crit=%s bulk=%s\n",
|
||||
tdma::mhz_of(c.sched.nb_w), c.sched.nb_ms, tdma::mhz_of(c.sched.wide_w),
|
||||
c.sched.wide_ms, "crit", "bulk");
|
||||
|
||||
while (!g_devourer_should_stop) {
|
||||
auto a = c.sched.at(tdma::wall_ms());
|
||||
ChannelWidth_t w = c.sched.width(a.phase);
|
||||
if (w != cur_w) { dev->FastSetBandwidth(w); cur_w = w; }
|
||||
|
||||
if (a.phase == tdma::Phase::NB && a.burst != last_marker_burst) {
|
||||
last_marker_burst = a.burst;
|
||||
// Stamp the marker with the TX's hardware TSF (works TX-side — no RX
|
||||
// flood starving the control read); the TSF-sync RX uses it for drift.
|
||||
uint64_t tx_tsf = dev->ReadTsf();
|
||||
auto f = tdma::build_frame(rt_marker, tdma::Class::Marker, seq[0]++,
|
||||
(uint32_t)a.burst, tx_tsf);
|
||||
dev->send_packet(f.data(), f.size());
|
||||
}
|
||||
tdma::Class cls =
|
||||
a.phase == tdma::Phase::NB ? tdma::Class::Critical : tdma::Class::Bulk;
|
||||
const auto& rt = cls == tdma::Class::Critical ? rt_crit : rt_bulk;
|
||||
auto f = tdma::build_frame(rt, cls, seq[(int)cls]++, (uint32_t)a.burst);
|
||||
dev->send_packet(f.data(), f.size());
|
||||
|
||||
if (std::chrono::steady_clock::now() >= next_stat) {
|
||||
next_stat += std::chrono::seconds(1);
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf),
|
||||
"{\"ev\":\"tdma.tx\",\"marker\":%u,\"critical\":%u,"
|
||||
"\"bulk\":%u,\"width_mhz\":%d}\n",
|
||||
seq[0], seq[1], seq[2], tdma::mhz_of(cur_w));
|
||||
emit(buf);
|
||||
}
|
||||
if (c.gap_us > 0) sleep_us(c.gap_us);
|
||||
}
|
||||
}
|
||||
|
||||
// --- RX roles ---------------------------------------------------------------
|
||||
static void rx_callback(const Packet& p) {
|
||||
auto pr = tdma::parse_frame(p.Data.data(), p.Data.size());
|
||||
if (!pr.ok || p.RxAtrib.crc_err) return;
|
||||
int nb = g_rx_mhz.load(std::memory_order_relaxed) <= 10 ? 1 : 0;
|
||||
g_cnt[nb][(int)pr.cls].fetch_add(1, std::memory_order_relaxed);
|
||||
const int64_t host = steady_ns();
|
||||
|
||||
if (g_tsf_mode) {
|
||||
// Feed EVERY frame into the host↔TSF fit; anchor the schedule on the
|
||||
// marker's hardware TSF (de-jittered through the fit), not the callback time.
|
||||
// Host time in microseconds so the least-squares sums stay well within
|
||||
// double precision (ns would overflow it over a multi-second run).
|
||||
std::lock_guard<std::mutex> lk(g_clock_mu);
|
||||
int64_t rx_tsf = g_clock.add(p.RxAtrib.tsfl, host / 1000);
|
||||
if (pr.cls == tdma::Class::Marker) {
|
||||
g_marker_tsf.store(rx_tsf, std::memory_order_relaxed);
|
||||
g_have_anchor.store(true, std::memory_order_relaxed);
|
||||
if (getenv("TDMA_DBG") && g_clock.ready())
|
||||
fprintf(stderr, "[mk] rx_tsf=%lld host_at=%.0f host_us=%lld resid=%.0f us\n",
|
||||
(long long)rx_tsf, g_clock.host_at(rx_tsf), (long long)(host / 1000),
|
||||
g_clock.host_at(rx_tsf) - host / 1000.0);
|
||||
if (pr.tx_tsf && g_prev_tx_tsf) { // crystal drift, TX TSF vs RX TSF
|
||||
double dtx = (double)(pr.tx_tsf - g_prev_tx_tsf);
|
||||
double drx = (double)(rx_tsf - g_prev_rx_tsf);
|
||||
double ppm = drx > 0 ? (dtx / drx - 1.0) * 1e6 : 1e9;
|
||||
if (dtx > 0 && ppm > -500 && ppm < 500) g_drift_ppm.store(ppm); // sane only
|
||||
}
|
||||
g_prev_tx_tsf = pr.tx_tsf;
|
||||
g_prev_rx_tsf = rx_tsf;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pr.cls == tdma::Class::Marker) { // marker (steady-clock) anchor
|
||||
g_marker_anchor_ns.store(host, std::memory_order_relaxed);
|
||||
g_have_anchor.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
// Desired RX width `guard` ms ahead, per the sync mode.
|
||||
static ChannelWidth_t desired_width(const tdma::Config& c) {
|
||||
if (c.role == tdma::Role::RxCamp) return c.camp_w;
|
||||
if (c.sync == tdma::Sync::WallClock) {
|
||||
auto a = c.sched.at(tdma::wall_ms() + c.guard_ms);
|
||||
return c.sched.width(a.phase);
|
||||
}
|
||||
// marker / tsf: camp narrowband until a marker anchors us, then coast.
|
||||
if (!g_have_anchor.load(std::memory_order_relaxed)) return c.sched.nb_w;
|
||||
int64_t elapsed_ms;
|
||||
if (c.sync == tdma::Sync::Tsf) {
|
||||
// Anchor = the marker's hardware TSF mapped through the fit to a de-jittered
|
||||
// host time (the ~1 ms callback jitter averaged out). Fit is in microseconds.
|
||||
std::lock_guard<std::mutex> lk(g_clock_mu);
|
||||
if (!g_clock.ready()) return c.sched.nb_w; // fit still warming up
|
||||
int64_t mt = g_marker_tsf.load(std::memory_order_relaxed);
|
||||
double anchor_us = g_clock.host_at(mt);
|
||||
double now_us = steady_ns() / 1000.0;
|
||||
elapsed_ms = (int64_t)((now_us - anchor_us) / 1000.0);
|
||||
static int dbg = 0;
|
||||
if (getenv("TDMA_DBG") && (dbg++ % 200 == 0))
|
||||
fprintf(stderr, "[dbg] n=%lld mt=%lld anchor_us=%.0f now_us=%.0f elapsed_ms=%lld\n",
|
||||
g_clock.n, (long long)mt, anchor_us, now_us, (long long)elapsed_ms);
|
||||
} else {
|
||||
elapsed_ms =
|
||||
(steady_ns() - g_marker_anchor_ns.load(std::memory_order_relaxed)) / 1000000;
|
||||
}
|
||||
if (elapsed_ms < 0 || elapsed_ms > 3LL * c.sched.period()) { // stale — re-acquire
|
||||
g_have_anchor.store(false, std::memory_order_relaxed);
|
||||
return c.sched.nb_w;
|
||||
}
|
||||
int pos = (int)((elapsed_ms + c.guard_ms) % c.sched.period());
|
||||
return pos < c.sched.nb_ms ? c.sched.nb_w : c.sched.wide_w;
|
||||
}
|
||||
|
||||
static void run_rx(IRadio* dev, const tdma::Config& c) {
|
||||
// Bring RX up: rx-camp at its band; rx-sync wide (the control loop corrects).
|
||||
ChannelWidth_t start_w = c.role == tdma::Role::RxCamp ? c.camp_w : CHANNEL_WIDTH_20;
|
||||
g_rx_mhz.store(tdma::mhz_of(start_w));
|
||||
std::thread rx([&] { dev->Init(rx_callback, SelectedChannel{c.channel, 0, start_w}); });
|
||||
|
||||
const char* role = c.role == tdma::Role::RxCamp ? "rx-camp"
|
||||
: c.sync == tdma::Sync::Tsf ? "rx-sync/tsf"
|
||||
: c.sync == tdma::Sync::Marker ? "rx-sync/marker"
|
||||
: "rx-sync/wallclock";
|
||||
fprintf(stderr, "tdma %s: start %dMHz nb=%dMHz wide=%dMHz guard=%dms\n", role,
|
||||
tdma::mhz_of(start_w), tdma::mhz_of(c.sched.nb_w),
|
||||
tdma::mhz_of(c.sched.wide_w), c.guard_ms);
|
||||
|
||||
// Let Init finish bring-up before the control thread touches the RF registers
|
||||
// — a FastSetBandwidth racing the bring-up corrupts a half-configured chip.
|
||||
for (int i = 0; i < 250 && !g_devourer_should_stop; ++i)
|
||||
sleep_us(10000); // ~2.5 s settle
|
||||
|
||||
ChannelWidth_t cur_w = start_w;
|
||||
auto next_stat = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
while (!g_devourer_should_stop) {
|
||||
if (c.role == tdma::Role::RxSync) {
|
||||
ChannelWidth_t w = desired_width(c);
|
||||
if (w != cur_w) {
|
||||
dev->FastSetBandwidth(w);
|
||||
cur_w = w;
|
||||
g_rx_mhz.store(tdma::mhz_of(w), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
if (std::chrono::steady_clock::now() >= next_stat) {
|
||||
next_stat += std::chrono::seconds(1);
|
||||
char buf[320];
|
||||
std::snprintf(
|
||||
buf, sizeof(buf),
|
||||
"{\"ev\":\"tdma.rx\",\"nb_marker\":%llu,\"nb_critical\":%llu,"
|
||||
"\"nb_bulk\":%llu,\"wide_marker\":%llu,\"wide_critical\":%llu,"
|
||||
"\"wide_bulk\":%llu,\"width_mhz\":%d}\n",
|
||||
(unsigned long long)g_cnt[1][0].load(), (unsigned long long)g_cnt[1][1].load(),
|
||||
(unsigned long long)g_cnt[1][2].load(), (unsigned long long)g_cnt[0][0].load(),
|
||||
(unsigned long long)g_cnt[0][1].load(), (unsigned long long)g_cnt[0][2].load(),
|
||||
tdma::mhz_of(cur_w));
|
||||
emit(buf);
|
||||
}
|
||||
sleep_us(2000);
|
||||
}
|
||||
dev->StopRxLoop();
|
||||
rx.join();
|
||||
|
||||
// Final summary: per (RX phase-band, class). In lockstep, critical+marker land
|
||||
// under the narrowband band, bulk under wide; leakage is the off-diagonal.
|
||||
fprintf(stderr,
|
||||
"\n=== tdma %s summary ===\n"
|
||||
" marker critical bulk\n"
|
||||
" narrowband %8llu %8llu %8llu\n"
|
||||
" wide %8llu %8llu %8llu\n",
|
||||
role, (unsigned long long)g_cnt[1][0].load(),
|
||||
(unsigned long long)g_cnt[1][1].load(), (unsigned long long)g_cnt[1][2].load(),
|
||||
(unsigned long long)g_cnt[0][0].load(), (unsigned long long)g_cnt[0][1].load(),
|
||||
(unsigned long long)g_cnt[0][2].load());
|
||||
const uint64_t correct = g_cnt[1][1].load() + g_cnt[0][2].load(); // crit@NB + bulk@wide
|
||||
const uint64_t wrong = g_cnt[0][1].load() + g_cnt[1][2].load(); // off-diagonal
|
||||
fprintf(stderr, " delivered(correct)=%llu off-diagonal=%llu",
|
||||
(unsigned long long)correct, (unsigned long long)wrong);
|
||||
if (g_tsf_mode) {
|
||||
double d = g_drift_ppm.load();
|
||||
if (d < 1e8) fprintf(stderr, " TX↔RX drift=%.1f ppm", d);
|
||||
else fprintf(stderr, " TX↔RX drift=n/a (TX TSF read starved under send load)");
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
|
||||
int main() {
|
||||
auto logger = std::make_shared<Logger>();
|
||||
apply_logging_env(*logger);
|
||||
install_devourer_signal_handlers();
|
||||
|
||||
// Owns the teardown order (device -> interface -> handle -> context; see
|
||||
// DeviceSession.h): the role loops below only return once their RX thread is
|
||||
// joined, so the adapter is released with nothing in flight.
|
||||
devourer::DeviceSession session{logger};
|
||||
|
||||
tdma::Config c = tdma::config_from_env();
|
||||
g_tsf_mode = (c.role == tdma::Role::RxSync && c.sync == tdma::Sync::Tsf);
|
||||
|
||||
libusb_context* ctx = nullptr;
|
||||
std::shared_ptr<devourer::UsbDeviceLock> lock;
|
||||
auto* handle = open_device(logger, &ctx, lock);
|
||||
session.adopt_context(ctx);
|
||||
if (!handle) return 1;
|
||||
session.adopt_handle(handle, devourer::find_wifi_interface(handle));
|
||||
session.adopt_lock(lock);
|
||||
|
||||
WiFiDriver wifi(logger);
|
||||
auto owned_device =
|
||||
wifi.CreateRadio(handle, ctx, lock, devourer_config_from_env());
|
||||
if (!owned_device) { logger->error("no driver for this chip"); return 1; }
|
||||
// The session owns the device from here: it is what guarantees the device
|
||||
// (and its in-flight TX) dies before libusb does.
|
||||
session.adopt_device(std::move(owned_device));
|
||||
IRadio* const dev = session.device();
|
||||
|
||||
if (c.role == tdma::Role::Tx) run_tx(dev, c);
|
||||
else run_rx(dev, c);
|
||||
session.close();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
// Burst-level bandwidth-TDMA support: schedule math, the on-air frame tag, and
|
||||
// the env-var config — self-contained (app-level, no WiFiDriver core changes),
|
||||
// shared by the tx / rx-sync / rx-camp roles in main.cpp.
|
||||
//
|
||||
// The idea (see docs/narrowband.md, "Burst-level bandwidth TDMA"): a transmitter
|
||||
// alternates BURSTS between a robust narrowband width (5/10 MHz — ~6 dB link
|
||||
// budget, for critical frames) and a wide width (20/40 MHz — throughput, for
|
||||
// bulk frames), flipping bandwidth with the cheap IRadio::FastSetBandwidth.
|
||||
// Narrowband is an ADC-clock-domain state, not a per-packet radiotap field, and
|
||||
// a receiver decodes exactly one width at a time — so the scheme is inherently
|
||||
// burst-level and the hard part is schedule synchronization.
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "RadiotapBuilder.h" // devourer::build_stream_radiotap / parse_tx_mode_str
|
||||
#include "SelectedChannel.h" // ChannelWidth_t
|
||||
#include "TxMode.h"
|
||||
|
||||
namespace tdma {
|
||||
|
||||
// --- Frame class ------------------------------------------------------------
|
||||
// Marker frames are emitted at each narrowband-burst start; the marker-sync RX
|
||||
// aligns its local schedule to them. Critical rides the narrowband burst, Bulk
|
||||
// the wide burst.
|
||||
enum class Class : uint8_t { Marker = 0, Critical = 1, Bulk = 2 };
|
||||
|
||||
inline const char* class_name(Class c) {
|
||||
switch (c) {
|
||||
case Class::Marker: return "marker";
|
||||
case Class::Critical: return "critical";
|
||||
case Class::Bulk: return "bulk";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
// --- On-air frame -----------------------------------------------------------
|
||||
// The canonical devourer beacon layout (SA 57:42:75:05:d6:00, so existing SA
|
||||
// matchers/tests recognise it) followed by a "TD" tag. Offsets below are into
|
||||
// the RX-side Packet.Data (the 802.11 MPDU, radiotap already stripped):
|
||||
// [0..1] FC (0x40 probe-req) [4..9] addr1 (broadcast)
|
||||
// [10..15] addr2 = SA [24..] the TD tag
|
||||
static const uint8_t kSa[6] = {0x57, 0x42, 0x75, 0x05, 0xd6, 0x00};
|
||||
static constexpr size_t kHdrLen = 24; // 802.11 header up to the body
|
||||
// 'T''D' ver cls seq[4] burst[4] tx_tsf[8]. The 8-byte TX-TSF stamp lets the
|
||||
// TSF-sync RX measure the TX↔RX crystal drift (0 when the TX can't read TSF).
|
||||
// v2 appends host_ns[8] — the transmitter's steady_clock at the send_packet
|
||||
// call, so a witness can fit air-arrival directly against the HOST clock (the
|
||||
// quantity a host-side slot scheduler actually controls), uncontaminated by
|
||||
// the ReadTsf control-read round-trip. v1 frames stay parseable.
|
||||
static constexpr size_t kTagLen = 20;
|
||||
static constexpr size_t kTagLenV2 = 28;
|
||||
static constexpr size_t kMinData = kHdrLen + kTagLen;
|
||||
|
||||
// Build a full TX buffer: [radiotap for this class][802.11 header][TD tag].
|
||||
// A nonzero host_ns selects the v2 tag.
|
||||
inline std::vector<uint8_t> build_frame(const std::vector<uint8_t>& radiotap,
|
||||
Class cls, uint32_t seq, uint32_t burst,
|
||||
uint64_t tx_tsf = 0,
|
||||
uint64_t host_ns = 0) {
|
||||
static const uint8_t hdr[kHdrLen] = {
|
||||
0x40, 0x00, 0x00, 0x00, // FC + duration
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // addr1 broadcast
|
||||
0x57, 0x42, 0x75, 0x05, 0xd6, 0x00, // addr2 = SA
|
||||
0x57, 0x42, 0x75, 0x05, 0xd6, 0x00, // addr3
|
||||
0x80, 0x00}; // seq ctl
|
||||
const size_t tag_len = host_ns ? kTagLenV2 : kTagLen;
|
||||
std::vector<uint8_t> f;
|
||||
f.reserve(radiotap.size() + kHdrLen + tag_len);
|
||||
f.insert(f.end(), radiotap.begin(), radiotap.end());
|
||||
f.insert(f.end(), hdr, hdr + kHdrLen);
|
||||
uint8_t tag[kTagLenV2] = {
|
||||
'T', 'D', static_cast<uint8_t>(host_ns ? 2 : 1), static_cast<uint8_t>(cls),
|
||||
static_cast<uint8_t>(seq), static_cast<uint8_t>(seq >> 8),
|
||||
static_cast<uint8_t>(seq >> 16), static_cast<uint8_t>(seq >> 24),
|
||||
static_cast<uint8_t>(burst), static_cast<uint8_t>(burst >> 8),
|
||||
static_cast<uint8_t>(burst >> 16),static_cast<uint8_t>(burst >> 24)};
|
||||
for (int i = 0; i < 8; ++i) tag[12 + i] = static_cast<uint8_t>(tx_tsf >> (8 * i));
|
||||
for (int i = 0; i < 8; ++i) tag[20 + i] = static_cast<uint8_t>(host_ns >> (8 * i));
|
||||
f.insert(f.end(), tag, tag + tag_len);
|
||||
return f;
|
||||
}
|
||||
|
||||
struct Parsed {
|
||||
bool ok = false;
|
||||
uint8_t ver = 0;
|
||||
Class cls = Class::Bulk;
|
||||
uint32_t seq = 0;
|
||||
uint32_t burst = 0;
|
||||
uint64_t tx_tsf = 0;
|
||||
uint64_t host_ns = 0; // v2 only: TX host steady_clock at send (0 on v1)
|
||||
};
|
||||
|
||||
// Parse an RX Packet.Data span (802.11 MPDU) into a TD tag, if it is one of ours.
|
||||
inline Parsed parse_frame(const uint8_t* data, size_t len) {
|
||||
Parsed p;
|
||||
if (len < kMinData) return p;
|
||||
if (std::memcmp(data + 10, kSa, 6) != 0) return p; // not our SA
|
||||
const uint8_t* t = data + kHdrLen;
|
||||
if (t[0] != 'T' || t[1] != 'D') return p;
|
||||
p.ver = t[2];
|
||||
p.cls = static_cast<Class>(t[3]);
|
||||
p.seq = static_cast<uint32_t>(t[4]) | (static_cast<uint32_t>(t[5]) << 8) |
|
||||
(static_cast<uint32_t>(t[6]) << 16) | (static_cast<uint32_t>(t[7]) << 24);
|
||||
p.burst = static_cast<uint32_t>(t[8]) | (static_cast<uint32_t>(t[9]) << 8) |
|
||||
(static_cast<uint32_t>(t[10]) << 16) | (static_cast<uint32_t>(t[11]) << 24);
|
||||
for (int i = 0; i < 8; ++i)
|
||||
p.tx_tsf |= static_cast<uint64_t>(t[12 + i]) << (8 * i);
|
||||
if (p.ver >= 2 && len >= kHdrLen + kTagLenV2)
|
||||
for (int i = 0; i < 8; ++i)
|
||||
p.host_ns |= static_cast<uint64_t>(t[20 + i]) << (8 * i);
|
||||
p.ok = true;
|
||||
return p;
|
||||
}
|
||||
|
||||
// --- TSF clock: a running host↔hardware-TSF least-squares fit ---------------
|
||||
// Every RX frame carries a hardware TSF (rx_pkt_attrib::tsfl, latched in the MAC
|
||||
// at receive) and a host time (when our callback ran). The host time is noisy
|
||||
// (USB batching + scheduling, ~1 ms RMS on Jaguar1); the TSF is not. Fitting
|
||||
// host = a·tsf + b over many frames averages the noise out, so evaluating the
|
||||
// line at a marker's tsf gives a de-jittered host time for that marker — the
|
||||
// precise schedule anchor. All state in offset coords (relative to the first
|
||||
// sample) to keep the sums numerically well-conditioned.
|
||||
struct TsfClock {
|
||||
bool init = false;
|
||||
double x0 = 0, y0 = 0;
|
||||
long long n = 0;
|
||||
double sx = 0, sy = 0, sxx = 0, sxy = 0;
|
||||
int64_t hi = 0; // 32→64-bit tsf reconstruction (low word wraps)
|
||||
uint32_t plo = 0;
|
||||
|
||||
int64_t recon(uint32_t lo) {
|
||||
if (init && lo < plo) hi += (1LL << 32);
|
||||
plo = lo;
|
||||
return hi + lo;
|
||||
}
|
||||
// Feed one frame; returns the reconstructed 64-bit tsf (µs).
|
||||
int64_t add(uint32_t tsfl, int64_t host_ns) {
|
||||
int64_t t = recon(tsfl);
|
||||
if (!init) { x0 = (double)t; y0 = (double)host_ns; init = true; }
|
||||
double xi = (double)t - x0, yi = (double)host_ns - y0;
|
||||
++n; sx += xi; sy += yi; sxx += xi * xi; sxy += xi * yi;
|
||||
return t;
|
||||
}
|
||||
bool ready() const { return n >= 16; }
|
||||
// The de-jittered host time (ns) for a given reconstructed tsf.
|
||||
double host_at(int64_t t) const {
|
||||
double den = (double)n * sxx - sx * sx;
|
||||
if (den == 0) return y0;
|
||||
double a = ((double)n * sxy - sx * sy) / den;
|
||||
double b = (sy - a * sx) / (double)n;
|
||||
return y0 + a * ((double)t - x0) + b;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Schedule ---------------------------------------------------------------
|
||||
enum class Phase { NB, WIDE };
|
||||
|
||||
inline int64_t wall_ms() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
struct Schedule {
|
||||
int nb_ms = 100;
|
||||
int wide_ms = 100;
|
||||
ChannelWidth_t nb_w = CHANNEL_WIDTH_10;
|
||||
ChannelWidth_t wide_w = CHANNEL_WIDTH_20;
|
||||
int64_t epoch_ms = 0; // shared wall-clock anchor (both ends must match)
|
||||
|
||||
int period() const { return nb_ms + wide_ms; }
|
||||
|
||||
// Phase active at wall-clock instant `t_ms`, plus the burst (period) index.
|
||||
struct At { Phase phase; int64_t burst; int ms_into_phase; };
|
||||
At at(int64_t t_ms) const {
|
||||
int64_t rel = t_ms - epoch_ms;
|
||||
int64_t burst = rel >= 0 ? rel / period() : (rel - period() + 1) / period();
|
||||
int pos = static_cast<int>(((rel % period()) + period()) % period());
|
||||
if (pos < nb_ms) return {Phase::NB, burst, pos};
|
||||
return {Phase::WIDE, burst, pos - nb_ms};
|
||||
}
|
||||
ChannelWidth_t width(Phase p) const { return p == Phase::NB ? nb_w : wide_w; }
|
||||
};
|
||||
|
||||
// --- Config (env) -----------------------------------------------------------
|
||||
enum class Role { Tx, RxSync, RxCamp };
|
||||
enum class Sync { WallClock, Marker, Tsf };
|
||||
|
||||
struct Config {
|
||||
Role role = Role::Tx;
|
||||
Sync sync = Sync::WallClock;
|
||||
Schedule sched;
|
||||
ChannelWidth_t camp_w = CHANNEL_WIDTH_10; // rx-camp fixed width
|
||||
int guard_ms = 20; // rx-sync: switch this early
|
||||
int gap_us = 500; // tx: intra-burst inter-frame gap
|
||||
devourer::TxMode crit_rate; // default 6M
|
||||
devourer::TxMode bulk_rate; // default MCS7
|
||||
uint8_t channel = 36;
|
||||
};
|
||||
|
||||
inline ChannelWidth_t width_of(int mhz) {
|
||||
switch (mhz) {
|
||||
case 5: return CHANNEL_WIDTH_5;
|
||||
case 10: return CHANNEL_WIDTH_10;
|
||||
case 40: return CHANNEL_WIDTH_40;
|
||||
default: return CHANNEL_WIDTH_20;
|
||||
}
|
||||
}
|
||||
inline int mhz_of(ChannelWidth_t w) {
|
||||
switch (w) {
|
||||
case CHANNEL_WIDTH_5: return 5;
|
||||
case CHANNEL_WIDTH_10: return 10;
|
||||
case CHANNEL_WIDTH_40: return 40;
|
||||
default: return 20;
|
||||
}
|
||||
}
|
||||
|
||||
inline int env_int(const char* n, int dflt) {
|
||||
const char* e = std::getenv(n);
|
||||
return (e && *e) ? std::atoi(e) : dflt;
|
||||
}
|
||||
|
||||
inline Config config_from_env() {
|
||||
Config c;
|
||||
if (const char* r = std::getenv("DEVOURER_TDMA_ROLE")) {
|
||||
std::string s(r);
|
||||
if (s == "rx-sync") c.role = Role::RxSync;
|
||||
else if (s == "rx-camp") c.role = Role::RxCamp;
|
||||
else c.role = Role::Tx;
|
||||
}
|
||||
if (const char* s = std::getenv("DEVOURER_TDMA_SYNC")) {
|
||||
std::string v(s);
|
||||
c.sync = v == "marker" ? Sync::Marker
|
||||
: v == "tsf" ? Sync::Tsf
|
||||
: Sync::WallClock;
|
||||
}
|
||||
|
||||
c.sched.nb_w = width_of(env_int("DEVOURER_TDMA_NB", 10));
|
||||
c.sched.wide_w = width_of(env_int("DEVOURER_TDMA_WIDE", 20));
|
||||
c.sched.nb_ms = env_int("DEVOURER_TDMA_NB_MS", 100);
|
||||
c.sched.wide_ms = env_int("DEVOURER_TDMA_WIDE_MS", 100);
|
||||
c.sched.epoch_ms = env_int("DEVOURER_TDMA_EPOCH_MS", 0);
|
||||
c.camp_w = width_of(env_int("DEVOURER_TDMA_CAMP", 10));
|
||||
c.guard_ms = env_int("DEVOURER_TDMA_GUARD_MS", 20);
|
||||
c.gap_us = env_int("DEVOURER_TDMA_GAP_US", 500);
|
||||
c.channel = static_cast<uint8_t>(env_int("DEVOURER_CHANNEL", 36));
|
||||
|
||||
const char* cr = std::getenv("DEVOURER_TDMA_CRIT_RATE");
|
||||
const char* br = std::getenv("DEVOURER_TDMA_BULK_RATE");
|
||||
c.crit_rate = devourer::parse_tx_mode_str(cr && *cr ? cr : "6M");
|
||||
c.bulk_rate = devourer::parse_tx_mode_str(br && *br ? br : "MCS7");
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace tdma
|
||||
Reference in New Issue
Block a user