From: CicadaTAK Date: Thu, 23 Oct 2026 00:00:00 +0000 Subject: [PATCH] batman-adv: v: weight a WiFi neighbour's ELP throughput by its post-retry delivery CicadaTAK-Marker: CICADATAK-BATADV-D1 batadv_v_elp_get_throughput() takes cfg80211's expected throughput as a WiFi link's metric. For minstrel_ht that is per-attempt goodput: the best rate's packets per second times its per-attempt success average. It has no term for frames lost after their retries, so a link whose driver gives up early can read better than a path that delivers. CicadaTAK measured a 2.4 GHz mesh link (mt76x0u) failing 32 % of frames after retries while its estimate rose from 5.7 to 8.5 Mbps; batman kept routing over it in preference to a HaLow path, and a teammate's positions and texts did not arrive. For WiFi drivers on a list (default: mt76x0u), keep a per-neighbour delivery EWMA from the station's cumulative tx_packets and tx_failed counters, in samples of at least 48 frames, falling by a quarter of the gap and recovering by an eighth (rounded), and report throughput x delivery^k. Nothing new is sent: the counters come from the same cfg80211_get_station() call, and ELP's existing probes keep an idle link sampled. A throughput_override still wins, as before. The weighted value never drops below min(throughput, 10), batman's own 1.0 Mbps for a link it cannot measure: the hop penalty truncates at every forward and a receiver drops an OGM whose throughput is 0, so a weight of 1 would cut the nodes beyond the next hop off a route that may be their only path. A station that is re-created keeps its neighbour's delivery estimate (only the counter baseline restarts), so a flapping link carries its penalty. cicadatak_delivery_drivers (string; entries split by commas or whitespace, so a trailing newline from a sysfs write still matches) and cicadatak_delivery_exp (uint, default 2; 0 restores stock behaviour at runtime) are 0644 module parameters. k = 2 is FIELD-calibrated on two points (2026-10-21 AM must not flip, 2026-10-23 must), n = 2, static, against the estimator's Monte Carlo steady state, not the raw delivery. Plan: cicadatak.git docs/plans/weak-24-route-metric.md. --- --- a/net/batman-adv/bat_v_elp.c +++ b/net/batman-adv/bat_v_elp.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,8 @@ #include #include #include +#include +#include #include #include #include @@ -42,7 +45,59 @@ #include "originator.h" #include "routing.h" #include "send.h" +#include "cicadatak_delivery.h" + +/* CicadaTAK: see cicadatak_delivery.h. Which WiFi drivers' ELP throughput is + * weighted by measured post-retry delivery: the 2.4 GHz Panda (mt76x0u) only. + * HaLow (morse) is excluded in CicadaTAK's dev #32: its estimate is per-attempt + * too, but MMRC's deep retry chain keeps its post-retry loss low, and until the + * odd-length MCAST alignment fix (902) is proven, those failed frames would + * inflate HaLow's tx_failed and penalise the wrong radio. + * Read without a lock: a sysfs write races one ELP sample at worst. + */ +static char cicadatak_delivery_drivers[64] = "mt76x0u"; +module_param_string(cicadatak_delivery_drivers, cicadatak_delivery_drivers, + sizeof(cicadatak_delivery_drivers), 0644); +MODULE_PARM_DESC(cicadatak_delivery_drivers, + "CicadaTAK: WiFi drivers whose ELP throughput is weighted by post-retry delivery (comma, space or newline separated; empty = off, stock)"); + +/* k = 2: FIELD-calibrated on two points (2026-10-21 AM must not flip, + * 2026-10-23 must), n = 2, static, against the ESTIMATOR's seeded Monte Carlo + * steady state (98 %: 0.948; 68 %: 0.435, under the 0.55 flip bound), not + * against the raw delivery. Not measured while moving and not at house density. + * The Monte Carlo assumes per-frame counters. Under a BlockAck session mt76x02 + * merges up to 32 statuses, so d reads optimistic by a load-varying amount: + * no calibration claim past these two points until W2 reads agg_status. + * The A/B knob: 0 = stock (docs/plans/weak-24-route-metric.md). + */ +static unsigned int cicadatak_delivery_exp = 2; +module_param(cicadatak_delivery_exp, uint, 0644); +MODULE_PARM_DESC(cicadatak_delivery_exp, + "CicadaTAK: the exponent k in throughput x delivery^k (0 = off, stock; at most 8)"); +MODULE_INFO(cicadatak, "CICADATAK-BATADV-D1"); + +/** + * batadv_v_elp_delivery_weighted() - is this WiFi netdev's driver on the list? + * @real_netdev: the hard interface's real net_device + * + * Return: true when its throughput is to be weighted by post-retry delivery. + */ +static bool batadv_v_elp_delivery_weighted(struct net_device *real_netdev) +{ + struct device *parent = real_netdev->dev.parent; + const struct device_driver *drv; + if (!READ_ONCE(cicadatak_delivery_exp) || !parent) + return false; + /* One read of ->driver. The caller calls this BEFORE its dev_put: while + * real_netdev is held, a USB unbind's netdev_wait_allrefs() waits for + * that reference before device_unbind_cleanup() clears ->driver. + */ + drv = READ_ONCE(parent->driver); + return drv && cicadatak_delivery_listed(cicadatak_delivery_drivers, + drv->name); +} + /** * struct batadv_v_metric_queue_entry - list of hardif neighbors which require * and metric update @@ -86,6 +141,7 @@ struct ethtool_link_ksettings link_settings; struct net_device *real_netdev; struct station_info sinfo; + bool weighted = false; u32 throughput; int ret; @@ -129,6 +185,7 @@ if (!ret) { /* free the TID stats immediately */ cfg80211_sinfo_release_content(&sinfo); + weighted = batadv_v_elp_delivery_weighted(real_netdev); } dev_put(real_netdev); @@ -143,8 +200,19 @@ if (ret) goto default_throughput; + if (weighted && + (sinfo.filled & BIT_ULL(NL80211_STA_INFO_TX_PACKETS)) && + (sinfo.filled & BIT_ULL(NL80211_STA_INFO_TX_FAILED))) + cicadatak_delivery_update(&neigh->bat_v.delivery, + sinfo.tx_packets, + sinfo.tx_failed); + if (sinfo.filled & BIT(NL80211_STA_INFO_EXPECTED_THROUGHPUT)) { *pthroughput = sinfo.expected_throughput / 100; + if (weighted) + *pthroughput = cicadatak_delivery_weight(*pthroughput, + &neigh->bat_v.delivery, + READ_ONCE(cicadatak_delivery_exp)); return true; } @@ -153,6 +221,10 @@ */ if (sinfo.filled & BIT(NL80211_STA_INFO_TX_BITRATE)) { *pthroughput = cfg80211_calculate_bitrate(&sinfo.txrate) / 3; + if (weighted) + *pthroughput = cicadatak_delivery_weight(*pthroughput, + &neigh->bat_v.delivery, + READ_ONCE(cicadatak_delivery_exp)); return true; } --- /dev/null +++ b/net/batman-adv/cicadatak_delivery.h @@ -0,0 +1,159 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * CicadaTAK (CICADATAK-BATADV-D1): weight a WiFi neighbour's ELP throughput by + * the delivery it actually achieves after retries. + * + * BATMAN_V takes cfg80211's expected throughput as a WiFi link's metric + * (bat_v_elp.c batadv_v_elp_get_throughput). minstrel_ht's expected throughput + * is per-ATTEMPT goodput: the best rate's packets per second times its + * per-attempt success average. It has no term for frames lost after their + * retries. On a 2.4 GHz mesh link whose driver gives up early, that number can + * rise while a third of the frames are lost: CicadaTAK measured 32 % of frames + * failed after retries while the estimate rose from 5.7 to 8.5 Mbps, and the + * route kept the link over a HaLow path that delivered (2026-10-23, FIELD). + * + * This keeps a per-neighbour delivery EWMA from the station's own cumulative + * tx_packets / tx_failed counters, in samples of at least + * CICADATAK_D_MIN_FRAMES frames. It falls by a quarter of the gap per sample + * and recovers by an eighth (both rounded to nearest), so a link's idle probe + * traffic heals a link whose data frames fail more slowly than it fell. The + * metric is throughput x d^k, never below min(throughput, CICADATAK_D_FLOOR). + * + * The asymmetry biases the steady state below the true delivery, by more when + * samples are small (noisy). 48 frames per sample keeps that bias small: by a + * seeded Monte Carlo of this exact arithmetic, a link delivering 98 % sits at + * d^2 = 0.948 (p05 0.923; the truth is 0.960), idle or loaded, and a 68 % link at + * 0.435 (p95 0.482). The first cut (16 frames, 1/2 down, 1/16 up) sat at 0.83 + * (p05 0.73) for a 98 % link with k = 3: a 17-26 % penalty on a healthy link. + * + * Pure (no kernel headers) so CicadaTAK's host test compiles this exact text. + */ +#ifndef _NET_BATMAN_ADV_CICADATAK_DELIVERY_H_ +#define _NET_BATMAN_ADV_CICADATAK_DELIVERY_H_ + +#define CICADATAK_D_ONE 1024U /* delivery 1.0 in this fixed point */ +#define CICADATAK_D_MIN_FRAMES 48U /* frames accumulated per sample */ +#define CICADATAK_D_EXP_MAX 8U /* the knob's ceiling */ +/* The lowest weighted metric, 1.0 Mbps: batman's own value for a link it + * cannot measure (BATADV_THROUGHPUT_DEFAULT_VALUE). PROTOCOL-derived, not + * RF-measured: batman forwards min(link, OGM) through a hop penalty that + * truncates (x 225/255 at the default hop_penalty 30), and a receiver drops + * an OGM whose throughput is 0. So a link weighted down to 1 would cut every + * node beyond the next hop off what may be their only path; 10 survives eight + * forwards, as stock's does. The cost: a HaLow path slower than ~1 Mbps cannot + * beat a near-dead 2.4 GHz link. + */ +#define CICADATAK_D_FLOOR 10U + +struct cicadatak_delivery { + unsigned int last_packets; /* counters at the last reading */ + unsigned int last_failed; + unsigned int acc_packets; /* accumulated toward the next sample */ + unsigned int acc_failed; + unsigned int d; /* the EWMA, 0..CICADATAK_D_ONE */ + unsigned char primed; /* last_* hold a baseline */ + unsigned char has_sample; /* d holds at least one sample */ +}; + +/* Feed one reading of the station's cumulative @packets and @failed counters. + * The first reading, and any reading where a counter went backwards (the + * station was re-created), only take a baseline. A re-baseline keeps d: a + * kick or a reconnect does not wipe a link's penalty, so a link that flaps + * cannot escape it. From the bottom, 18 clean samples bring d back to 0.9 + * (~216 s on an idle link: two ELP probes per 500 ms, 48 frames a sample). + * batman frees the neighbour, d with it, once it has heard nothing from it + * for BATADV_PURGE_TIMEOUT (200 s); a new one starts at stock weight. + */ +static inline void cicadatak_delivery_update(struct cicadatak_delivery *s, + unsigned int packets, + unsigned int failed) +{ + unsigned int sample; + + if (!s->primed || packets < s->last_packets || + failed < s->last_failed) { + s->last_packets = packets; + s->last_failed = failed; + s->acc_packets = 0; + s->acc_failed = 0; + s->primed = 1; + return; + } + s->acc_packets += packets - s->last_packets; + s->acc_failed += failed - s->last_failed; + s->last_packets = packets; + s->last_failed = failed; + if (s->acc_packets < CICADATAK_D_MIN_FRAMES) + return; + if (s->acc_failed > s->acc_packets) + s->acc_failed = s->acc_packets; + sample = CICADATAK_D_ONE - + (unsigned int)((unsigned long long)s->acc_failed * + CICADATAK_D_ONE / s->acc_packets); + if (!s->has_sample) { + s->d = sample; + s->has_sample = 1; + } else if (sample < s->d) { + s->d -= (s->d - sample + 2) / 4; /* falls by 1/4 */ + } else { + s->d += (sample - s->d + 4) / 8; /* recovers by 1/8 */ + } + s->acc_packets = 0; + s->acc_failed = 0; +} + +/* @throughput x d^@exp, never below min(@throughput, CICADATAK_D_FLOOR). + * @exp 0 (the knob's "off") and a neighbour with no sample yet return + * @throughput unchanged: no evidence is not bad evidence. + */ +static inline unsigned int +cicadatak_delivery_weight(unsigned int throughput, + const struct cicadatak_delivery *s, unsigned int exp) +{ + unsigned long long t = throughput; + unsigned int i, floor; + + if (!exp || !s->has_sample) + return throughput; + if (exp > CICADATAK_D_EXP_MAX) + exp = CICADATAK_D_EXP_MAX; + for (i = 0; i < exp; i++) + t = t * s->d / CICADATAK_D_ONE; + floor = throughput < CICADATAK_D_FLOOR ? throughput : CICADATAK_D_FLOOR; + return t < floor ? floor : (unsigned int)t; +} + +/* A list separator: a comma, or any character C's isspace() accepts. A sysfs + * write keeps its newline (`echo mt76x0u > ...` stores "mt76x0u\n"), and a + * list may be space- or tab-separated as well as comma-separated. + */ +static inline int cicadatak_delivery_sep(char c) +{ + return c == ',' || c == ' ' || c == '\t' || c == '\n' || + c == '\v' || c == '\f' || c == '\r'; +} + +/* Nonzero when @name is one entry of @list, entries split by any run of + * separators (exact match). An empty list, or an empty name, matches nothing. + */ +static inline int cicadatak_delivery_listed(const char *list, const char *name) +{ + const char *p = list, *n; + + if (!p || !name || !*name) + return 0; + while (*p) { + while (cicadatak_delivery_sep(*p)) + p++; + for (n = name; *p && !cicadatak_delivery_sep(*p) && *p == *n; + p++, n++) + ; + if (!*n && (!*p || cicadatak_delivery_sep(*p))) + return 1; + while (*p && !cicadatak_delivery_sep(*p)) + p++; + } + return 0; +} + +#endif /* _NET_BATMAN_ADV_CICADATAK_DELIVERY_H_ */ --- a/net/batman-adv/types.h +++ b/net/batman-adv/types.h @@ -30,6 +30,8 @@ #include #include #include + +#include "cicadatak_delivery.h" #ifdef CONFIG_BATMAN_ADV_DAT @@ -606,6 +608,12 @@ * neighbor */ unsigned long last_unicast_tx; + + /** + * @delivery: CicadaTAK - post-retry delivery towards this neighbor, + * from its station counters (cicadatak_delivery.h) + */ + struct cicadatak_delivery delivery; }; /**