This commit is contained in:
2026-09-13 13:30:21 +08:00
commit a6bbd520cf
2744 changed files with 1598795 additions and 0 deletions
+334
View File
@@ -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;
}
+261
View File
@@ -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