adding more metric reporting #27

Merged
Koda merged 1 commits from metrics into main 2026-07-14 23:09:21 +01:00
5 changed files with 573 additions and 42 deletions
+11
View File
@@ -167,6 +167,11 @@ int clients_read_wifi(const char *wlan_iface,
if (sscanf(line, " tx bytes: %lld", &llv) == 1) { cur->tx_bytes = llv; continue; }
if (sscanf(line, " rx packets: %lld", &llv) == 1) { cur->rx_packets = llv; continue; }
if (sscanf(line, " tx packets: %lld", &llv) == 1) { cur->tx_packets = llv; continue; }
if (sscanf(line, " tx retries: %lld", &llv) == 1) { cur->tx_retries = llv; continue; }
if (sscanf(line, " tx failed: %lld", &llv) == 1) { cur->tx_failed = llv; continue; }
if (sscanf(line, " rx drop misc: %lld", &llv) == 1) { cur->rx_dropped = llv; continue; }
if (sscanf(line, " tx duration: %lld us", &llv) == 1) { cur->tx_duration = llv; continue; }
if (sscanf(line, " rx duration: %lld us", &llv) == 1) { cur->rx_duration = llv; continue; }
/* ── Signal ───────────────────────────────────────────────── */
int sig;
@@ -266,6 +271,12 @@ struct json_object *clients_build_sta_table(const char *wlan_iface,
json_object_object_add(o, "rx_bytes", json_object_new_int64(s->rx_bytes));
json_object_object_add(o, "tx_packets", json_object_new_int64(s->tx_packets));
json_object_object_add(o, "rx_packets", json_object_new_int64(s->rx_packets));
json_object_object_add(o, "tx_retries", json_object_new_int64(s->tx_retries));
json_object_object_add(o, "tx_failed", json_object_new_int64(s->tx_failed));
json_object_object_add(o, "tx_dropped", json_object_new_int64(s->tx_failed));
json_object_object_add(o, "rx_dropped", json_object_new_int64(s->rx_dropped));
json_object_object_add(o, "tx_duration", json_object_new_int64(s->tx_duration));
json_object_object_add(o, "rx_duration", json_object_new_int64(s->rx_duration));
json_object_object_add(o, "uptime", json_object_new_int(s->uptime));
json_object_object_add(o, "radio", json_object_new_string(s->radio));
json_object_object_add(o, "channel", json_object_new_int(s->channel));
+5
View File
@@ -55,6 +55,11 @@ typedef struct {
long long rx_bytes;
long long tx_packets;
long long rx_packets;
long long tx_retries;
long long tx_failed;
long long rx_dropped;
long long tx_duration;
long long rx_duration;
int uptime; /* seconds online */
char radio[8]; /* "ng" / "na" / "6g" */
int channel;
+266 -24
View File
@@ -155,9 +155,11 @@ static void debug_log_controller_response(struct json_object *response,
The controller shows CPU and RAM in the device view.
We read /proc/stat and /proc/meminfo directly.
*/
static struct json_object *build_sys_stats(void)
static struct json_object *build_sys_stats(int *cpu_percent,
double *mem_percent)
{
struct json_object *o = json_object_new_object();
*mem_percent = 0.0;
mem_stats_t mem;
if (sysinfo_mem(&mem) == 0) {
@@ -176,10 +178,38 @@ static struct json_object *build_sys_stats(void)
json_object_object_add(o, "mem_buffer", json_object_new_int(0));
}
/* CPU — delta relative to the previous call (every ~10s gives a good average) */
json_object_object_add(o, "cpu",
json_object_new_int(sysinfo_cpu_percent()));
if (mem.total_kb > 0)
*mem_percent = 100.0 * (double)(mem.total_kb - mem.free_kb -
mem.buffer_kb - mem.cached_kb) / (double)mem.total_kb;
FILE *loadavg = fopen("/proc/loadavg", "r");
if (loadavg) {
char one[16], five[16], fifteen[16];
if (fscanf(loadavg, "%15s %15s %15s", one, five, fifteen) == 3) {
json_object_object_add(o, "loadavg_1", json_object_new_string(one));
json_object_object_add(o, "loadavg_5", json_object_new_string(five));
json_object_object_add(o, "loadavg_15", json_object_new_string(fifteen));
}
fclose(loadavg);
}
*cpu_percent = sysinfo_cpu_percent();
return o;
}
static struct json_object *build_system_stats(int cpu_percent,
double mem_percent,
long uptime)
{
struct json_object *o = json_object_new_object();
char value[32];
snprintf(value, sizeof(value), "%.1f", (double)cpu_percent);
json_object_object_add(o, "cpu", json_object_new_string(value));
snprintf(value, sizeof(value), "%.1f", mem_percent);
json_object_object_add(o, "mem", json_object_new_string(value));
snprintf(value, sizeof(value), "%ld", uptime);
json_object_object_add(o, "uptime", json_object_new_string(value));
return o;
}
@@ -238,26 +268,126 @@ static struct json_object *build_if_table(const uf_model_t *m,
return arr;
}
static void radio_runtime_iface(const uf_model_t *m, const char *band,
char *out, size_t out_size)
{
const char *device = wlan_device_for_band(m, band);
int phy_index = 0;
if (device) sscanf(device, "radio%d", &phy_index);
snprintf(out, out_size, "phy%d-ap0", phy_index);
struct json_object *vaps = wlan_get_vap_table(m);
int count = json_object_array_length(vaps);
for (int i = 0; i < count; i++) {
struct json_object *vap = json_object_array_get_idx(vaps, i);
struct json_object *radio_obj, *ifname_obj;
if (json_object_object_get_ex(vap, "radio", &radio_obj) &&
json_object_object_get_ex(vap, "ifname", &ifname_obj) &&
!strcmp(json_object_get_string(radio_obj), band)) {
snprintf(out, out_size, "%s", json_object_get_string(ifname_obj));
break;
}
}
json_object_put(vaps);
}
static struct json_object *build_scan_table(const char *iface)
{
wifi_scan_t scans[MAX_SCAN_RESULTS];
int count = sysinfo_wifi_scan_cache(iface, scans, MAX_SCAN_RESULTS);
struct json_object *arr = json_object_new_array();
for (int i = 0; i < count; i++) {
struct json_object *o = json_object_new_object();
json_object_object_add(o, "age", json_object_new_int(scans[i].age));
json_object_object_add(o, "bssid", json_object_new_string(scans[i].bssid));
json_object_object_add(o, "essid", json_object_new_string(scans[i].essid));
json_object_object_add(o, "freq", json_object_new_int(scans[i].frequency));
json_object_object_add(o, "channel", json_object_new_int(scans[i].channel));
json_object_object_add(o, "signal", json_object_new_int(scans[i].signal));
json_object_object_add(o, "rssi", json_object_new_int(scans[i].rssi));
json_object_object_add(o, "security", json_object_new_string(
scans[i].secured ? "secured" : "open"));
json_object_object_add(o, "is_adhoc", json_object_new_boolean(false));
json_object_object_add(o, "is_ubnt", json_object_new_boolean(false));
json_object_array_add(arr, o);
}
return arr;
}
/* ═══════════════════════════════════════════════════════════════════
radio_table — static definition of the radio hardware
═══════════════════════════════════════════════════════════════════
Describes the physical capabilities of each radio to the controller.
The controller uses this to know which frequencies and modes it supports.
*/
static struct json_object *build_athstats(struct json_object *stats,
const char *radio_name)
{
struct json_object *o = json_object_new_object();
struct json_object *v;
json_object_object_add(o, "name", json_object_new_string(radio_name));
json_object_object_add(o, "noise_floor", json_object_new_int(-95));
json_object_object_add(o, "satisfaction", json_object_new_int(-1));
json_object_object_add(o, "satisfaction_now", json_object_new_int(-1));
json_object_object_add(o, "satisfaction_real", json_object_new_int(-1));
static const char *const fields[] = {
"cu_total", "cu_self_rx", "cu_self_tx", "tx_packets", "tx_retries"
};
for (size_t i = 0; i < sizeof(fields) / sizeof(fields[0]); i++) {
if (stats && json_object_object_get_ex(stats, fields[i], &v))
json_object_object_add(o, fields[i], json_object_get(v));
else
json_object_object_add(o, fields[i], json_object_new_int(0));
}
if (stats && json_object_object_get_ex(stats, "noise", &v)) {
json_object_object_del(o, "noise_floor");
json_object_object_add(o, "noise_floor", json_object_get(v));
}
return o;
}
static void build_radio_table(struct json_object *root,
const uf_model_t *m)
const uf_model_t *m,
struct json_object *radio_stats)
{
struct json_object *arr = json_object_new_array();
for (int i = 0; i < m->radio_table_len; i++) {
const uf_radio_t *r = &m->radio_table[i];
struct json_object *o = json_object_new_object();
struct json_object *stats = NULL;
struct json_object *value;
if (radio_stats && i < json_object_array_length(radio_stats))
stats = json_object_array_get_idx(radio_stats, i);
int nss = r->nss;
int tx_antennas = r->nss;
int rx_antennas = r->nss;
if (stats && json_object_object_get_ex(stats, "nss", &value) &&
json_object_get_int(value) > 0)
nss = json_object_get_int(value);
if (stats && json_object_object_get_ex(stats, "num_tx_antennas", &value) &&
json_object_get_int(value) > 0)
tx_antennas = json_object_get_int(value);
if (stats && json_object_object_get_ex(stats, "num_rx_antennas", &value) &&
json_object_get_int(value) > 0)
rx_antennas = json_object_get_int(value);
char mimo[16];
snprintf(mimo, sizeof(mimo), "%dx%d", tx_antennas, rx_antennas);
json_object_object_add(o, "name", json_object_new_string(r->name));
json_object_object_add(o, "radio", json_object_new_string(r->radio));
json_object_object_add(o, "channel", json_object_new_int(r->channel));
json_object_object_add(o, "ht", json_object_new_string(r->ht));
json_object_object_add(o, "min_txpower", json_object_new_int(r->min_txpower));
json_object_object_add(o, "max_txpower", json_object_new_int(r->max_txpower));
json_object_object_add(o, "nss", json_object_new_int(r->nss));
json_object_object_add(o, "nss", json_object_new_int(nss));
json_object_object_add(o, "max_nss", json_object_new_int(nss));
json_object_object_add(o, "num_tx_antennas", json_object_new_int(tx_antennas));
json_object_object_add(o, "num_rx_antennas", json_object_new_int(rx_antennas));
json_object_object_add(o, "mimo", json_object_new_string(mimo));
json_object_object_add(o, "tx_power", json_object_new_int(r->tx_power));
json_object_object_add(o, "radio_caps", json_object_new_int(r->radio_caps));
json_object_object_add(o, "radio_caps2", json_object_new_int(r->radio_caps2));
@@ -265,7 +395,24 @@ static void build_radio_table(struct json_object *root,
json_object_object_add(o, "he_enabled", json_object_new_boolean(r->he_enabled));
json_object_object_add(o, "builtin_antenna", json_object_new_boolean(true));
json_object_object_add(o, "builtin_ant_gain", json_object_new_int(0));
json_object_object_add(o, "athstats", build_athstats(stats, r->name));
char iface[32];
radio_runtime_iface(m, r->radio, iface, sizeof(iface));
json_object_object_add(o, "scan_table", build_scan_table(iface));
/*
* Native UniFi device records expose both radio_table[] and a
* top-level radio_<band> alias. Some controller views resolve
* capabilities such as NSS/MIMO through the alias.
*/
char alias[16];
snprintf(alias, sizeof(alias), "radio_%s", r->radio);
json_object_object_add(root, alias, json_object_get(o));
json_object_array_add(arr, o);
if (protocol_debug_level > 0)
LOG("Protocol radio capability: name=%s band=%s nss=%d tx_chains=%d rx_chains=%d",
r->name, r->radio, nss, tx_antennas, rx_antennas);
}
json_object_object_add(root, "radio_table", arr);
}
@@ -284,14 +431,8 @@ static struct json_object *build_radio_table_stats(const uf_model_t *m)
for (int i = 0; i < m->radio_map_len; i++) {
const uf_radio_map_t *rm = &m->radio_map[i];
const char *device = wlan_device_for_band(m, rm->band);
if (!device) device = rm->device;
/* Map "radio0" → "wlan0" by OpenWrt convention */
char wlan_iface[32];
int ridx = 0;
sscanf(device, "radio%d", &ridx);
snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx);
radio_runtime_iface(m, rm->band, wlan_iface, sizeof(wlan_iface));
/* Radio name in the static table */
const char *radio_name = (i < m->radio_table_len)
@@ -300,16 +441,32 @@ static struct json_object *build_radio_table_stats(const uf_model_t *m)
? m->radio_table[i].channel : 6;
int default_pwr = (i < m->radio_table_len)
? m->radio_table[i].tx_power : 20;
int nss = (i < m->radio_table_len)
? m->radio_table[i].nss : 1;
radio_stats_t rs;
if (sysinfo_radio(wlan_iface, &rs) != 0) {
memset(&rs, 0, sizeof(rs));
rs.noise = -95;
}
if (rs.nss > 0) nss = rs.nss;
int tx_antennas = rs.tx_antennas > 0 ? rs.tx_antennas : nss;
int rx_antennas = rs.rx_antennas > 0 ? rs.rx_antennas : nss;
char mimo[16];
snprintf(mimo, sizeof(mimo), "%dx%d", tx_antennas, rx_antennas);
struct json_object *o = json_object_new_object();
json_object_object_add(o, "name",
json_object_new_string(radio_name));
json_object_object_add(o, "radio",
json_object_new_string(rm->band));
json_object_object_add(o, "state",
json_object_new_string("RUN"));
json_object_object_add(o, "nss", json_object_new_int(nss));
json_object_object_add(o, "max_nss", json_object_new_int(nss));
json_object_object_add(o, "num_tx_antennas", json_object_new_int(tx_antennas));
json_object_object_add(o, "num_rx_antennas", json_object_new_int(rx_antennas));
json_object_object_add(o, "mimo", json_object_new_string(mimo));
json_object_object_add(o, "channel",
json_object_new_int(rs.channel ? rs.channel : default_ch));
json_object_object_add(o, "tx_power",
@@ -324,6 +481,16 @@ static struct json_object *build_radio_table_stats(const uf_model_t *m)
json_object_new_int(rs.num_sta));
json_object_object_add(o, "noise",
json_object_new_int(rs.noise));
json_object_object_add(o, "tx_packets",
json_object_new_int64(rs.tx_packets));
json_object_object_add(o, "tx_retries",
json_object_new_int64(rs.tx_retries));
json_object_object_add(o, "wifi_tx_dropped",
json_object_new_int64(rs.tx_failed));
json_object_object_add(o, "tx_duration",
json_object_new_int64(rs.tx_duration));
json_object_object_add(o, "rx_duration",
json_object_new_int64(rs.rx_duration));
json_object_array_add(arr, o);
}
return arr;
@@ -438,6 +605,20 @@ static struct json_object *build_vap_table(const uf_model_t *m)
if (json_object_object_get_ex(vap, "handoff_suggestions", &v))
handoff_suggestions = json_object_get_boolean(v);
const char *radio_name = radio;
int radio_nss = 1;
int radio_tx_antennas = 1;
int radio_rx_antennas = 1;
for (int j = 0; j < m->radio_table_len; j++) {
if (!strcmp(m->radio_table[j].radio, radio)) {
radio_name = m->radio_table[j].name;
radio_nss = m->radio_table[j].nss;
radio_tx_antennas = radio_nss;
radio_rx_antennas = radio_nss;
break;
}
}
/* Map band → wlan interface and current channel */
char wlan_iface[32] = "phy0-ap0";
if (ifname && ifname[0])
@@ -474,8 +655,14 @@ static struct json_object *build_vap_table(const uf_model_t *m)
/* Calculate tx_power of the corresponding radio */
int tx_pwr = 20;
radio_stats_t rs2;
if (sysinfo_radio(wlan_iface, &rs2) == 0 && rs2.tx_power)
tx_pwr = rs2.tx_power;
if (sysinfo_radio(wlan_iface, &rs2) == 0) {
if (rs2.tx_power) tx_pwr = rs2.tx_power;
if (rs2.nss > 0) radio_nss = rs2.nss;
if (rs2.tx_antennas > 0) radio_tx_antennas = rs2.tx_antennas;
if (rs2.rx_antennas > 0) radio_rx_antennas = rs2.rx_antennas;
}
char radio_mimo[16];
snprintf(radio_mimo, sizeof(radio_mimo), "%dx%d", radio_tx_antennas, radio_rx_antennas);
struct json_object *o = json_object_new_object();
json_object_object_add(o, "essid",
@@ -486,6 +673,15 @@ static struct json_object *build_vap_table(const uf_model_t *m)
json_object_new_string(vap_name));
json_object_object_add(o, "radio",
json_object_new_string(radio));
json_object_object_add(o, "radio_name",
json_object_new_string(radio_name));
json_object_object_add(o, "nss", json_object_new_int(radio_nss));
json_object_object_add(o, "max_nss", json_object_new_int(radio_nss));
json_object_object_add(o, "num_tx_antennas", json_object_new_int(radio_tx_antennas));
json_object_object_add(o, "num_rx_antennas", json_object_new_int(radio_rx_antennas));
json_object_object_add(o, "mimo", json_object_new_string(radio_mimo));
json_object_object_add(o, "state",
json_object_new_string(iface_st.up ? "RUN" : "INIT"));
if (vlan_id > 0)
json_object_object_add(o, "vlan_id", json_object_new_int(vlan_id));
json_object_object_add(o, "up",
@@ -518,6 +714,46 @@ static struct json_object *build_vap_table(const uf_model_t *m)
json_object_new_int64(iface_st.rx_dropped));
json_object_object_add(o, "tx_dropped",
json_object_new_int64(iface_st.tx_dropped));
long long tx_retries = 0, tx_failed = 0, rx_dropped = 0;
long long signal_sum = 0, ccq_sum = 0;
int signal_count = 0;
int sta_count = json_object_array_length(sta_tbl);
for (int j = 0; j < sta_count; j++) {
struct json_object *station = json_object_array_get_idx(sta_tbl, j);
struct json_object *counter;
if (json_object_object_get_ex(station, "signal", &counter) &&
json_object_get_int(counter) < 0) {
signal_sum += json_object_get_int(counter);
signal_count++;
}
if (json_object_object_get_ex(station, "ccq", &counter))
ccq_sum += json_object_get_int(counter);
if (json_object_object_get_ex(station, "tx_retries", &counter))
tx_retries += json_object_get_int64(counter);
if (json_object_object_get_ex(station, "tx_failed", &counter))
tx_failed += json_object_get_int64(counter);
if (json_object_object_get_ex(station, "rx_dropped", &counter))
rx_dropped += json_object_get_int64(counter);
}
json_object_object_add(o, "avg_client_signal", json_object_new_int(
signal_count ? (int)(signal_sum / signal_count) : 0));
json_object_object_add(o, "num_satisfaction_sta",
json_object_new_int(signal_count));
long long tx_attempts = iface_st.tx_packets + tx_retries;
json_object_object_add(o, "tx_retries", json_object_new_int64(tx_retries));
json_object_object_add(o, "tx_combined_retries",
json_object_new_int64(tx_retries));
json_object_object_add(o, "tx_rts_retries", json_object_new_int(0));
json_object_object_add(o, "tx_total", json_object_new_int64(tx_attempts));
json_object_object_add(o, "tx_success",
json_object_new_int64(iface_st.tx_packets));
json_object_object_add(o, "wifi_tx_attempts",
json_object_new_int64(tx_attempts));
json_object_object_add(o, "wifi_tx_dropped", json_object_new_int64(tx_failed));
json_object_object_add(o, "rx_frags", json_object_new_int64(iface_st.rx_frame));
json_object_object_add(o, "rx_crypts", json_object_new_int(0));
json_object_object_add(o, "rx_nwids", json_object_new_int64(rx_dropped));
/* Only controller-issued ObjectIds are valid in this field. */
if (vap_id)
json_object_object_add(o, "id", json_object_new_string(vap_id));
@@ -527,7 +763,9 @@ static struct json_object *build_vap_table(const uf_model_t *m)
json_object_object_add(o, "usage",
json_object_new_string("user"));
json_object_object_add(o, "ccq",
json_object_new_int(0));
json_object_new_int(signal_count ? (int)(ccq_sum / signal_count) : 0));
json_object_object_add(o, "t",
json_object_new_string("vap"));
/* Nested sta_table — clients of THIS VAP */
json_object_object_add(o, "sta_table", sta_tbl);
@@ -642,18 +880,22 @@ static char *build_payload(const openuf_state_t *st,
json_object_object_add(root, "country_code",
json_object_new_int(0));
/* ── CPU + RAM ──────────────────────────────────────────────── */
json_object_object_add(root, "sys_stats", build_sys_stats());
/* Memory/load and percentage stats use distinct UniFi schemas. */
int cpu_percent;
double mem_percent;
json_object_object_add(root, "sys_stats",
build_sys_stats(&cpu_percent, &mem_percent));
json_object_object_add(root, "system-stats",
build_system_stats(cpu_percent, mem_percent, uptime));
/* ── Ethernet interfaces with real counters ──────────────── */
json_object_object_add(root, "if_table", build_if_table(m, st));
/* ── Radio capabilities (static, from the model) ─────────────── */
build_radio_table(root, m);
/* ── Real-time channel utilization ────────────────────── */
json_object_object_add(root, "radio_table_stats",
build_radio_table_stats(m));
/* Native informs carry RF counters in radio_table[].athstats. Keep the
* normalized table too for controller versions that consume it directly. */
struct json_object *radio_stats = build_radio_table_stats(m);
build_radio_table(root, m, radio_stats);
json_object_object_add(root, "radio_table_stats", radio_stats);
/* ── Ethernet ports with actual status ───────────────────────── */
build_port_table(root, m);
+266 -18
View File
@@ -38,6 +38,7 @@
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <time.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
@@ -200,6 +201,7 @@ int sysinfo_iface(const char *ifname, iface_stats_t *out)
&tb,&tp,&te,&td,&tf,&tcol,&tcomp,&tcarr);
out->rx_bytes = rb; out->rx_packets = rp;
out->rx_errors = re; out->rx_dropped = rd;
out->rx_frame = rframe;
out->rx_multicast= rmulti;
out->tx_bytes = tb; out->tx_packets = tp;
out->tx_errors = te; out->tx_dropped = td;
@@ -213,6 +215,46 @@ int sysinfo_iface(const char *ifname, iface_stats_t *out)
WiFi Radio
═══════════════════════════════════════════════════════════════════
Survey counters are cumulative. Keep a small per-interface snapshot so
each inform reports utilization during the latest interval rather than an
average since the radio was started.
*/
typedef struct {
char iface[32];
long long active;
long long busy;
long long tx;
long long rx;
long long sta_tx_duration;
long long sta_rx_duration;
struct timespec duration_time;
int duration_valid;
int valid;
} survey_snapshot_t;
#define MAX_SURVEY_SNAPSHOTS 8
static survey_snapshot_t survey_snapshots[MAX_SURVEY_SNAPSHOTS];
static int utilization_percent(long long part, long long total)
{
if (part <= 0 || total <= 0) return 0;
long long value = (part * 100 + total / 2) / total;
if (value < 0) return 0;
if (value > 100) return 100;
return (int)value;
}
static int count_antenna_chains(unsigned int mask)
{
int count = 0;
while (mask) {
count += mask & 1U;
mask >>= 1;
}
return count;
}
/*
1. iw dev wlan0 info → channel and power
Example:
Interface wlan0
@@ -239,6 +281,8 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
out->noise = -95;
char cmd[128];
int current_freq = 0;
int wiphy_index = -1;
/* iw dev <iface> info */
snprintf(cmd, sizeof(cmd), "iw dev %s info 2>/dev/null", iface);
@@ -248,30 +292,83 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
char line[256];
while (fgets(line, sizeof(line), p)) {
int ch; float mhz;
if (sscanf(line, " channel %d (%f MHz)", &ch, &mhz) == 2)
if (sscanf(line, " channel %d (%f MHz)", &ch, &mhz) == 2) {
out->channel = ch;
current_freq = (int)(mhz + 0.5f);
}
int index;
if (sscanf(line, " wiphy %d", &index) == 1)
wiphy_index = index;
float tp;
if (sscanf(line, " txpower %f dBm", &tp) == 1)
out->tx_power = (int)tp;
}
pclose(p);
/* Read the physical radio rather than trusting the emulated model. */
if (wiphy_index >= 0) {
snprintf(cmd, sizeof(cmd), "iw phy phy%d info 2>/dev/null", wiphy_index);
p = popen(cmd, "r");
if (p) {
unsigned int available_tx = 0, available_rx = 0;
unsigned int configured_tx = 0, configured_rx = 0;
int max_mcs_nss = 0;
while (fgets(line, sizeof(line), p)) {
unsigned int tx_mask, rx_mask;
if (sscanf(line, " Configured Antennas: TX 0x%x RX 0x%x",
&tx_mask, &rx_mask) == 2) {
configured_tx = tx_mask;
configured_rx = rx_mask;
} else if (sscanf(line, " Available Antennas: TX 0x%x RX 0x%x",
&tx_mask, &rx_mask) == 2) {
available_tx = tx_mask;
available_rx = rx_mask;
}
int streams, top_mcs;
if (sscanf(line, " %d streams: MCS 0-%d", &streams, &top_mcs) == 2 &&
streams > max_mcs_nss)
max_mcs_nss = streams;
if (sscanf(line,
" HT TX/RX MCS rate indexes supported: 0-%d",
&top_mcs) == 1) {
int ht_nss = top_mcs / 8 + 1;
if (ht_nss > max_mcs_nss) max_mcs_nss = ht_nss;
}
}
pclose(p);
out->tx_antennas = count_antenna_chains(
configured_tx ? configured_tx : available_tx);
out->rx_antennas = count_antenna_chains(
configured_rx ? configured_rx : available_rx);
if (out->tx_antennas && out->rx_antennas)
out->nss = out->tx_antennas < out->rx_antennas
? out->tx_antennas : out->rx_antennas;
else
out->nss = max_mcs_nss;
}
}
/* iw dev <iface> survey dump */
snprintf(cmd, sizeof(cmd), "iw dev %s survey dump 2>/dev/null", iface);
p = popen(cmd, "r");
if (!p) return 0;
long long active=0, busy=0, tx_t=0, rx_t=0;
int in_use = 0;
int selected = 0;
while (fgets(line, sizeof(line), p)) {
if (strstr(line, "[in use]")) {
in_use = 1; active=busy=tx_t=rx_t=0; continue;
}
if (!in_use) continue;
/* New frequency without [in use] resets the block */
if (strstr(line, "frequency:") && !strstr(line, "[in use]")) {
in_use = 0; continue;
int survey_freq;
if (sscanf(line, " frequency: %d MHz", &survey_freq) == 1) {
/* Some drivers omit the optional [in use] marker, especially on
* the secondary radio. Match the frequency reported by iw info. */
selected = strstr(line, "[in use]") != NULL ||
(current_freq > 0 && survey_freq == current_freq);
if (selected)
active=busy=tx_t=rx_t=0;
continue;
}
if (!selected) continue;
float noise; long long val;
if (sscanf(line, " noise: %f dBm", &noise) == 1) out->noise = (int)noise;
if (sscanf(line, " channel active time: %lld ms", &val) == 1) active = val;
@@ -281,18 +378,169 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
}
pclose(p);
if (active > 0) {
out->cu_total = (int)(busy * 100 / active);
out->cu_self_tx = (int)(tx_t * 100 / active);
out->cu_self_rx = (int)(rx_t * 100 / active);
long long sample_active = active;
long long sample_busy = busy;
long long sample_tx = tx_t;
long long sample_rx = rx_t;
survey_snapshot_t *snapshot = NULL;
survey_snapshot_t *free_slot = NULL;
for (int i = 0; i < MAX_SURVEY_SNAPSHOTS; i++) {
if (survey_snapshots[i].valid &&
!strcmp(survey_snapshots[i].iface, iface)) {
snapshot = &survey_snapshots[i];
break;
}
if (!survey_snapshots[i].valid && !free_slot)
free_slot = &survey_snapshots[i];
}
if (!snapshot) snapshot = free_slot;
if (snapshot && snapshot->valid && active > snapshot->active &&
busy >= snapshot->busy && tx_t >= snapshot->tx && rx_t >= snapshot->rx) {
sample_active = active - snapshot->active;
sample_busy = busy - snapshot->busy;
sample_tx = tx_t - snapshot->tx;
sample_rx = rx_t - snapshot->rx;
}
/* Number of associated clients */
snprintf(cmd, sizeof(cmd),
"iw dev %s station dump 2>/dev/null | grep -c '^Station'",
iface);
if (snapshot && active > 0) {
snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
snapshot->active = active;
snapshot->busy = busy;
snapshot->tx = tx_t;
snapshot->rx = rx_t;
snapshot->valid = 1;
}
if (sample_active > 0) {
out->cu_total = utilization_percent(sample_busy, sample_active);
out->cu_self_tx = utilization_percent(sample_tx, sample_active);
out->cu_self_rx = utilization_percent(sample_rx, sample_active);
}
/* Associated clients and counters exposed by nl80211. */
snprintf(cmd, sizeof(cmd), "iw dev %s station dump 2>/dev/null", iface);
p = popen(cmd, "r");
if (p) { fscanf(p, "%d", &out->num_sta); pclose(p); }
if (p) {
while (fgets(line, sizeof(line), p)) {
long long val;
if (!strncmp(line, "Station ", 8)) {
out->num_sta++;
} else if (sscanf(line, " tx packets: %lld", &val) == 1) {
out->tx_packets += val;
} else if (sscanf(line, " tx retries: %lld", &val) == 1) {
out->tx_retries += val;
} else if (sscanf(line, " tx failed: %lld", &val) == 1) {
out->tx_failed += val;
} else if (sscanf(line, " tx duration: %lld us", &val) == 1) {
out->tx_duration += val;
} else if (sscanf(line, " rx duration: %lld us", &val) == 1) {
out->rx_duration += val;
}
}
pclose(p);
}
/* Some drivers expose no survey busy time on their secondary radio but
* do expose per-station airtime durations. Use those interval counters as
* a conservative "This AP" fallback; interference remains zero because
* station data cannot measure neighboring transmitters. */
struct timespec now;
if (snapshot && clock_gettime(CLOCK_MONOTONIC, &now) == 0) {
long long elapsed_us = 0;
if (snapshot->duration_valid) {
elapsed_us = (now.tv_sec - snapshot->duration_time.tv_sec) * 1000000LL +
(now.tv_nsec - snapshot->duration_time.tv_nsec) / 1000LL;
}
if (snapshot->duration_valid && elapsed_us >= 1000000LL &&
out->tx_duration >= snapshot->sta_tx_duration &&
out->rx_duration >= snapshot->sta_rx_duration &&
out->cu_total == 0) {
long long tx_delta = out->tx_duration - snapshot->sta_tx_duration;
long long rx_delta = out->rx_duration - snapshot->sta_rx_duration;
out->cu_self_tx = utilization_percent(tx_delta, elapsed_us);
out->cu_self_rx = utilization_percent(rx_delta, elapsed_us);
out->cu_total = out->cu_self_tx + out->cu_self_rx;
if (out->cu_total > 100) out->cu_total = 100;
}
/* sysinfo_radio() is called more than once while building an inform.
* Ignore sub-second calls so they do not replace the interval base. */
if (!snapshot->duration_valid || elapsed_us >= 1000000LL) {
if (!snapshot->valid) {
snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
snapshot->valid = 1;
}
snapshot->sta_tx_duration = out->tx_duration;
snapshot->sta_rx_duration = out->rx_duration;
snapshot->duration_time = now;
snapshot->duration_valid = 1;
}
}
return 0;
}
static int frequency_to_channel(int frequency)
{
if (frequency == 2484) return 14;
if (frequency >= 2412 && frequency <= 2472)
return (frequency - 2407) / 5;
if (frequency >= 5000 && frequency <= 5895)
return (frequency - 5000) / 5;
if (frequency >= 5955 && frequency <= 7115)
return (frequency - 5950) / 5;
return 0;
}
int sysinfo_wifi_scan_cache(const char *iface, wifi_scan_t *out, int max_out)
{
if (!iface || !out || max_out <= 0) return 0;
char cmd[128];
snprintf(cmd, sizeof(cmd), "iw dev %s scan dump 2>/dev/null", iface);
FILE *p = popen(cmd, "r");
if (!p) return 0;
int count = 0;
wifi_scan_t *cur = NULL;
char line[512];
while (fgets(line, sizeof(line), p)) {
char bssid[32];
if (sscanf(line, "BSS %31[^ (]", bssid) == 1) {
if (count >= max_out) {
cur = NULL;
continue;
}
cur = &out[count++];
memset(cur, 0, sizeof(*cur));
snprintf(cur->bssid, sizeof(cur->bssid), "%s", bssid);
continue;
}
if (!cur) continue;
int value;
float signal;
char essid[64];
if (sscanf(line, " freq: %d", &value) == 1) {
cur->frequency = value;
cur->channel = frequency_to_channel(value);
} else if (sscanf(line, " signal: %f dBm", &signal) == 1) {
cur->signal = (int)signal;
value = (cur->signal + 100) * 2;
cur->rssi = value < 0 ? 0 : value > 100 ? 100 : value;
} else if (sscanf(line, " last seen: %d ms ago", &value) == 1) {
cur->age = value / 1000;
} else if (sscanf(line, " SSID: %63[^\n]", essid) == 1) {
snprintf(cur->essid, sizeof(cur->essid), "%s", essid);
} else if (strstr(line, "capability:") && strstr(line, "Privacy")) {
cur->secured = true;
} else if (strstr(line, "RSN:") || strstr(line, "WPA:")) {
cur->secured = true;
}
}
pclose(p);
return count;
}
+25
View File
@@ -49,6 +49,7 @@ typedef struct {
long long tx_errors;
long long rx_dropped;
long long tx_dropped;
long long rx_frame;
long long rx_multicast;
} iface_stats_t;
@@ -65,9 +66,33 @@ typedef struct {
int cu_self_rx; /* % time spent receiving */
int num_sta;
int noise; /* dBm */
int nss; /* usable spatial streams */
int tx_antennas; /* configured TX chains */
int rx_antennas; /* configured RX chains */
long long tx_packets;
long long tx_retries;
long long tx_failed;
long long tx_duration; /* microseconds */
long long rx_duration; /* microseconds */
} radio_stats_t;
/* iface: "wlan0", "wlan1" */
int sysinfo_radio(const char *iface, radio_stats_t *out);
#define MAX_SCAN_RESULTS 128
typedef struct {
char bssid[32];
char essid[64];
int frequency;
int channel;
int signal; /* dBm */
int rssi; /* controller-compatible quality, 0-100 */
int age; /* seconds since last seen */
bool secured;
} wifi_scan_t;
/* Read the kernel's cached BSS list without starting a disruptive scan. */
int sysinfo_wifi_scan_cache(const char *iface, wifi_scan_t *out, int max_out);
#endif /* OPENUF_SYSINFO_H */