Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/cmake-multi-platform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ jobs:
WiFiDriver StreamTxDemo StreamDuplexDemo StreamStdinSelftest
ToneMaskSelftest BfReportDecodeSelftest SweepSpecSelftest
TxPowerQuantSelftest LinkHealthSelftest TxCapsSelftest TxPktPwrSelftest
RxQualitySelftest

- name: Test
working-directory: build
Expand Down
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ add_library(WiFiDriver
src/TxCaps.h
src/LinkHealth.cpp
src/LinkHealth.h
src/RxQuality.h
src/IRtlDevice.h
src/SignalStop.cpp
src/SignalStop.h
Expand Down Expand Up @@ -354,6 +355,15 @@ target_link_libraries(LinkHealthSelftest PRIVATE WiFiDriver)

add_test(NAME link_health_classify COMMAND LinkHealthSelftest)

# Headless guard for the RX link-quality feed (src/RxQuality.h) — the aggregate
# math + passive noise-floor formula + verdict fuse behind GetRxQuality().
add_executable(RxQualitySelftest
tests/rx_quality_selftest.cpp
)
target_link_libraries(RxQualitySelftest PRIVATE WiFiDriver)

add_test(NAME rx_quality_derive COMMAND RxQualitySelftest)

# Headless guard for the TX-capability derivation (src/TxCaps.h) — the
# STBC-needs-2-chains rule the send_packet 1T1R guard relies on.
add_executable(TxCapsSelftest
Expand Down
29 changes: 29 additions & 0 deletions demo/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ static const bool g_linkhealth = []() -> bool {
return e && *e && std::strcmp(e, "0") != 0;
}();

/* DEVOURER_RXQUALITY=1 — emit a <devourer-rxquality> line from the library's
* GetRxQuality() feed (the runtime API a linked adaptive-link controller reads).
* Rides the DEVOURER_RX_ENERGY_MS cadence like linkhealth. */
static const bool g_rxquality = []() -> bool {
const char *e = std::getenv("DEVOURER_RXQUALITY");
return e && *e && std::strcmp(e, "0") != 0;
}();

/* DEVOURER_RX_SWEEP="1,6,11" | "36-48/4" | "5170-5250/5": live coarse spectrum
* sweep. Cycle the listed bins (SweepSpec grammar: channels, channel ranges,
* or MHz ranges — the latter for issue-#149-style narrowband maps), dwelling
Expand Down Expand Up @@ -772,6 +780,27 @@ int main() {
h.igi_at_ceiling ? " igi_ceil=1" : "", h.cause, h.fix);
fflush(stdout);
}
/* DEVOURER_RXQUALITY=1 — dogfood the library GetRxQuality() feed: the
* same window a linked controller (fluke_gs) would read, incl. the
* passive noise-floor. NB it drains the device-internal accumulator +
* calls GetRxEnergy itself, so its counters are independent of the
* <devourer-energy>/<devourer-linkhealth> lines above (which use the
* demo's own g_rxagg). */
if (g_rxquality) {
devourer::RxQuality q = dev->GetRxQuality();
char nf[24], evmb[16];
if (q.nf_valid) std::snprintf(nf, sizeof nf, "%.1f", q.noise_floor_dbm);
else std::snprintf(nf, sizeof nf, "-");
if (q.evm_valid) std::snprintf(evmb, sizeof evmb, "%.1f", q.evm_mean_db);
else std::snprintf(evmb, sizeof evmb, "-");
printf("<devourer-rxquality>verdict=%s frames=%u rssi_mean_dbm=%d "
"rssi_max_dbm=%d snr_mean_db=%.1f snr_min_db=%.1f evm_db=%s "
"noise_floor_dbm=%s igi=%d\n",
q.label, q.frames, q.rssi_mean_dbm, q.rssi_max_dbm,
q.snr_mean_db, q.snr_min_db, evmb, nf,
q.igi_valid ? q.igi : -1);
fflush(stdout);
}
nap(g_rx_energy_ms);
}
});
Expand Down
12 changes: 12 additions & 0 deletions src/IRtlDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <cstdint>
#include <functional>

#include "RxQuality.h"
#include "RxSense.h"
#include "SelectedChannel.h"
#include "ThermalStatus.h"
Expand Down Expand Up @@ -163,6 +164,17 @@ class IRtlDevice {
* previous call. Default returns an all-invalid snapshot; each generation
* overrides with a real reader. */
virtual RxEnergy GetRxEnergy() { return {}; }

/* Consolidated windowed RX link-quality snapshot (see RxQuality.h) — the
* runtime feed a closed-loop adaptive-link controller reads instead of
* scraping the demo's stdout. Fuses the per-frame RSSI/SNR/EVM aggregate the
* device accumulates internally, a passive noise-floor estimate (rssi - snr,
* the self-jamming signal), the frame-free FA/CCA/IGI energy, and the
* LinkHealth verdict. Drains the window (delta semantics) and SUBSUMES
* GetRxEnergy (it calls it internally + consumes the FA/CCA delta — don't also
* poll GetRxEnergy separately on the same cadence). Default is an all-invalid
* snapshot; each generation overrides. */
virtual devourer::RxQuality GetRxQuality() { return {}; }
};

#endif /* IRTL_DEVICE_H */
208 changes: 208 additions & 0 deletions src/RxQuality.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/* Runtime RX link-quality feed — the consolidated per-window snapshot a
* closed-loop adaptive-link controller (e.g. an alink / Fluke-style GS) reads
* to steer the link, WITHOUT scraping the demo's stdout.
*
* devourer already exposes per-frame RSSI/SNR/EVM (the Packet callback) and
* frame-free FA/CCA/IGI (GetRxEnergy), but the windowing + the LinkHealth
* verdict lived only in demo/main.cpp. This promotes that into the library: the
* device aggregates per-frame RX quality internally, and GetRxQuality() returns
* one struct fusing the frame aggregate, the frame-free energy, a passive
* noise-floor estimate, and the plain-language verdict.
*
* NOISE FLOOR (passive): per decoded OFDM/HT frame, the effective noise+
* interference floor the receiver sees is nf_dbm = rssi_dbm - snr_db =
* (rssi_raw - 110) - snr_raw/2. It's a PASSIVE estimate — updated only when a
* wanted frame arrives — but that is exactly the self-jamming quantity: raising
* TX power on a near-field link raises reflections into the front end, SNR drops
* while RSSI holds, so this rises (the "decrease txpower until NF returns to
* normal" signal). Works on every family, including Jaguar3 where IGI can't
* (no background DIG). The frame-free ABSOLUTE idle floor is a separate, heavier
* measurement (see the active noise-floor path).
*/
#ifndef DEVOURER_RX_QUALITY_H
#define DEVOURER_RX_QUALITY_H

#include <cstdint>
#include <mutex>

#include "LinkHealth.h"
#include "RxSense.h"

namespace devourer {

/* Windowed RX link-quality snapshot. Angles: frame-driven aggregate
* (rssi/snr/evm/noise-floor), frame-free energy (fa/cca/igi), and the fused
* LinkHealth verdict. `valid` is false when no frames were decoded in the
* window. Converted units (dBm / dB) so a controller doesn't repeat the raw
* conversions. */
struct RxQuality {
bool valid = false;
uint32_t frames = 0;

int rssi_mean_dbm = 0; /* window mean of path-A PWDB (raw - 110) */
int rssi_max_dbm = 0; /* window peak — the strength signal (see LinkHealth) */
double snr_mean_db = 0.0;
double snr_min_db = 0.0;
double evm_mean_db = 0.0; /* 0 when evm_valid is false */
bool evm_valid = false;

/* Passive noise-floor estimate (dBm), mean of per-frame rssi_dbm - snr_db
* over OFDM/HT frames in the window. nf_valid false if none carried SNR. */
double noise_floor_dbm = 0.0;
bool nf_valid = false;

/* Frame-free energy (from GetRxEnergy — GetRxQuality subsumes it). */
bool energy_valid = false;
uint32_t fa_ofdm = 0;
uint32_t cca_ofdm = 0;
bool igi_valid = false;
int igi = 0;

/* Fused verdict (classify_link_health). */
LinkVerdict verdict = LinkVerdict::NoSignal;
const char *label = "NO_SIGNAL";
const char *cause = "";
const char *fix = "";
bool igi_at_floor = false;
bool igi_at_ceiling = false;
};

/* Drained per-window aggregate in raw devourer units (the accumulator's
* output). Kept separate from RxQuality so the unit conversion + verdict live
* in one place (build_rx_quality). */
struct RxQualitySnapshot {
uint32_t frames = 0;
int rssi_mean_raw = 0;
int rssi_max_raw = 0;
int snr_mean_raw = 0;
int snr_min_raw = 0;
int evm_mean_raw = 0;
bool evm_valid = false;
double nf_mean_dbm = 0.0;
bool nf_valid = false;
};

/* Thread-safe rolling per-frame accumulator. The device feeds add() from the RX
* loop for every decoded frame; a controller (or the energy emitter) drains it
* with snapshot() on its own cadence. Mirrors the demo's RxAgg with a
* passive-noise-floor term added. */
class RxQualityAccumulator {
public:
/* Raw path-A values straight off rx_pkt_attrib. A frame with no phy-status
* power (rssi_raw <= 0) is not a quality sample and is skipped. SNR/EVM are
* folded only when present (raw != 0 — CCK / non-type1 phy-status leaves them
* 0), so a mixed stream doesn't bias those means toward zero. */
void add(int rssi_raw, int snr_raw, int evm_raw) {
if (rssi_raw <= 0)
return;
std::lock_guard<std::mutex> lk(mu_);
++n_;
rssi_sum_ += rssi_raw;
if (rssi_raw > rssi_max_)
rssi_max_ = rssi_raw;
snr_sum_ += snr_raw;
if (snr_raw < snr_min_)
snr_min_ = snr_raw;
if (evm_raw != 0) {
evm_sum_ += evm_raw;
++evm_n_;
}
if (snr_raw != 0) {
nf_sum_ += (rssi_raw - 110) - snr_raw / 2.0;
++nf_n_;
}
}

/* Snapshot the window into raw aggregates and RESET (delta semantics, like
* GetRxEnergy). */
RxQualitySnapshot snapshot() {
RxQualitySnapshot s;
std::lock_guard<std::mutex> lk(mu_);
s.frames = n_;
if (n_) {
s.rssi_mean_raw = rssi_sum_ / static_cast<int>(n_);
s.rssi_max_raw = rssi_max_;
s.snr_mean_raw = snr_sum_ / static_cast<int>(n_);
s.snr_min_raw = snr_min_;
}
if (evm_n_) {
s.evm_mean_raw = evm_sum_ / static_cast<int>(evm_n_);
s.evm_valid = true;
}
if (nf_n_) {
s.nf_mean_dbm = nf_sum_ / static_cast<double>(nf_n_);
s.nf_valid = true;
}
n_ = 0;
rssi_sum_ = 0;
rssi_max_ = -128;
snr_sum_ = 0;
snr_min_ = 127;
evm_sum_ = 0;
evm_n_ = 0;
nf_sum_ = 0.0;
nf_n_ = 0;
return s;
}

private:
std::mutex mu_;
uint32_t n_ = 0;
int32_t rssi_sum_ = 0, rssi_max_ = -128;
int32_t snr_sum_ = 0, snr_min_ = 127;
int32_t evm_sum_ = 0;
uint32_t evm_n_ = 0;
double nf_sum_ = 0.0;
uint32_t nf_n_ = 0;
};

/* Fuse the frame aggregate + frame-free energy into an RxQuality, running the
* LinkHealth classifier once. Pure — no I/O; the device passes GetRxEnergy()'s
* result in. IGI rails are the union J1/J2 DIG floor (saturation corroborator)
* and the J3 ceiling (so a static J3 IGI never reads as a false 'weak' rail),
* matching the demo's LinkHealth mapping. */
inline RxQuality build_rx_quality(const RxQualitySnapshot &s, const RxEnergy &e,
const LinkHealthThresholds &th = {}) {
RxQuality q;
q.frames = s.frames;
q.valid = s.frames > 0;
q.rssi_mean_dbm = s.rssi_mean_raw - 110;
q.rssi_max_dbm = s.rssi_max_raw - 110;
q.snr_mean_db = s.snr_mean_raw / 2.0;
q.snr_min_db = s.snr_min_raw / 2.0;
q.evm_valid = s.evm_valid;
q.evm_mean_db = s.evm_mean_raw / 2.0;
q.noise_floor_dbm = s.nf_mean_dbm;
q.nf_valid = s.nf_valid;
q.energy_valid = e.valid_fa;
q.fa_ofdm = e.fa_ofdm;
q.cca_ofdm = e.cca_ofdm;
q.igi_valid = e.valid_igi;
q.igi = e.igi;

LinkHealthInput in;
in.frames = s.frames;
in.rssi_raw = s.frames ? s.rssi_max_raw : 0; /* strength = window peak */
in.snr_raw = s.snr_mean_raw;
in.evm_raw = s.evm_mean_raw;
in.evm_valid = s.evm_valid;
in.energy_valid = e.valid_fa;
in.fa_ofdm = e.fa_ofdm;
in.cca_ofdm = e.cca_ofdm;
in.igi_valid = e.valid_igi;
in.igi = e.igi;
in.igi_min = 0x1c;
in.igi_max = 0x7f;
LinkHealthVerdict h = classify_link_health(in, th);
q.verdict = h.verdict;
q.label = h.label;
q.cause = h.cause;
q.fix = h.fix;
q.igi_at_floor = h.igi_at_floor;
q.igi_at_ceiling = h.igi_at_ceiling;
return q;
}

} // namespace devourer

#endif /* DEVOURER_RX_QUALITY_H */
2 changes: 2 additions & 0 deletions src/jaguar1/RtlJaguarDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,8 @@ void RtlJaguarDevice::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
std::span<uint8_t>{const_cast<uint8_t *>(data), (size_t)n})) {
if (should_stop || g_devourer_should_stop)
break;
if (!p.RxAtrib.crc_err)
_rxq.add(p.RxAtrib.rssi[0], p.RxAtrib.snr[0], p.RxAtrib.evm[0]);
_packetProcessor(p);
}
};
Expand Down
11 changes: 11 additions & 0 deletions src/jaguar1/RtlJaguarDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ class RtlJaguarDevice : public IRtlDevice {
* state (an ApplyTxPower on a cold chip would write an unpowered BB). */
bool _brought_up = false;

/* Rolling per-frame RX link-quality aggregate (GetRxQuality). Fed from the RX
* loop for every decoded frame; drained by GetRxQuality on the caller's
* cadence. */
devourer::RxQualityAccumulator _rxq;

public:
RtlJaguarDevice(RtlUsbAdapter device, Logger_t logger);
~RtlJaguarDevice() override;
Expand Down Expand Up @@ -153,6 +158,12 @@ class RtlJaguarDevice : public IRtlDevice {
* also running it shares/steals these counters. */
RxEnergy GetRxEnergy() override;

/* Consolidated windowed RX link-quality snapshot (see RxQuality.h) — subsumes
* GetRxEnergy. Fed per decoded frame in the RX loop via _rxq. */
devourer::RxQuality GetRxQuality() override {
return devourer::build_rx_quality(_rxq.snapshot(), GetRxEnergy());
}

/* Runtime TX-mode default. send_packet honours a frame's own radiotap rate
* fields per-packet; when a frame's radiotap carries no rate, this mode
* supplies the modulation / MCS / BW / GI / FEC / STBC instead of the
Expand Down
2 changes: 2 additions & 0 deletions src/jaguar2/RtlJaguar2Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ void RtlJaguar2Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
f.drvinfo_size, f.rx_rate <= 3, p.RxAtrib);
p.Data =
std::span<uint8_t>(const_cast<uint8_t *>(f.frame), f.frame_len);
if (!p.RxAtrib.crc_err)
_rxq.add(p.RxAtrib.rssi[0], p.RxAtrib.snr[0], p.RxAtrib.evm[0]);
_packetProcessor(p);
}
++frames;
Expand Down
8 changes: 8 additions & 0 deletions src/jaguar2/RtlJaguar2Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,16 @@ class RtlJaguar2Device : public IRtlDevice {
* The read side of the CW tone. */
RxEnergy GetRxEnergy() override;

/* Consolidated windowed RX link-quality snapshot (see RxQuality.h) — subsumes
* GetRxEnergy. Fed per decoded frame in the RX loop via _rxq. */
devourer::RxQuality GetRxQuality() override {
return devourer::build_rx_quality(_rxq.snapshot(), GetRxEnergy());
}

private:
RtlUsbAdapter _device;
/* Rolling per-frame RX link-quality aggregate (GetRxQuality). */
devourer::RxQualityAccumulator _rxq;
Logger_t _logger;
jaguar2::ChipVariant _variant;
jaguar2::HalJaguar2 _hal;
Expand Down
2 changes: 2 additions & 0 deletions src/jaguar3/RtlJaguar3Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ void RtlJaguar3Device::StartRxLoop(Action_ParsedRadioPacket packetProcessor) {
jaguar3::parse_phy_sts_jgr3(data + off + jaguar3::RXDESC_SIZE_8822C,
f.drvinfo_size, p.RxAtrib);
p.Data = std::span<uint8_t>(const_cast<uint8_t *>(f.frame), f.frame_len);
if (!p.RxAtrib.crc_err)
_rxq.add(p.RxAtrib.rssi[0], p.RxAtrib.snr[0], p.RxAtrib.evm[0]);
_packetProcessor(p);
}
if (++frames <= 5)
Expand Down
Loading
Loading