adding more metric reporting (#27)

Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
2026-07-14 23:09:21 +01:00
parent b014e67753
commit b52a0a165d
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, " 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, " 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 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 ───────────────────────────────────────────────── */ /* ── Signal ───────────────────────────────────────────────── */
int sig; 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, "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, "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, "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, "uptime", json_object_new_int(s->uptime));
json_object_object_add(o, "radio", json_object_new_string(s->radio)); json_object_object_add(o, "radio", json_object_new_string(s->radio));
json_object_object_add(o, "channel", json_object_new_int(s->channel)); 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 rx_bytes;
long long tx_packets; long long tx_packets;
long long rx_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 */ int uptime; /* seconds online */
char radio[8]; /* "ng" / "na" / "6g" */ char radio[8]; /* "ng" / "na" / "6g" */
int channel; 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. The controller shows CPU and RAM in the device view.
We read /proc/stat and /proc/meminfo directly. 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(); struct json_object *o = json_object_new_object();
*mem_percent = 0.0;
mem_stats_t mem; mem_stats_t mem;
if (sysinfo_mem(&mem) == 0) { 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)); 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) */ if (mem.total_kb > 0)
json_object_object_add(o, "cpu", *mem_percent = 100.0 * (double)(mem.total_kb - mem.free_kb -
json_object_new_int(sysinfo_cpu_percent())); 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; return o;
} }
@@ -238,26 +268,126 @@ static struct json_object *build_if_table(const uf_model_t *m,
return arr; 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 radio_table — static definition of the radio hardware
═══════════════════════════════════════════════════════════════════ ═══════════════════════════════════════════════════════════════════
Describes the physical capabilities of each radio to the controller. Describes the physical capabilities of each radio to the controller.
The controller uses this to know which frequencies and modes it supports. 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, 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(); struct json_object *arr = json_object_new_array();
for (int i = 0; i < m->radio_table_len; i++) { for (int i = 0; i < m->radio_table_len; i++) {
const uf_radio_t *r = &m->radio_table[i]; const uf_radio_t *r = &m->radio_table[i];
struct json_object *o = json_object_new_object(); 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, "name", json_object_new_string(r->name));
json_object_object_add(o, "radio", json_object_new_string(r->radio)); 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, "channel", json_object_new_int(r->channel));
json_object_object_add(o, "ht", json_object_new_string(r->ht)); 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, "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, "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, "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_caps", json_object_new_int(r->radio_caps));
json_object_object_add(o, "radio_caps2", json_object_new_int(r->radio_caps2)); 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, "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_antenna", json_object_new_boolean(true));
json_object_object_add(o, "builtin_ant_gain", json_object_new_int(0)); 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); 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); 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++) { for (int i = 0; i < m->radio_map_len; i++) {
const uf_radio_map_t *rm = &m->radio_map[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]; char wlan_iface[32];
int ridx = 0; radio_runtime_iface(m, rm->band, wlan_iface, sizeof(wlan_iface));
sscanf(device, "radio%d", &ridx);
snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx);
/* Radio name in the static table */ /* Radio name in the static table */
const char *radio_name = (i < m->radio_table_len) 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; ? m->radio_table[i].channel : 6;
int default_pwr = (i < m->radio_table_len) int default_pwr = (i < m->radio_table_len)
? m->radio_table[i].tx_power : 20; ? m->radio_table[i].tx_power : 20;
int nss = (i < m->radio_table_len)
? m->radio_table[i].nss : 1;
radio_stats_t rs; radio_stats_t rs;
if (sysinfo_radio(wlan_iface, &rs) != 0) { if (sysinfo_radio(wlan_iface, &rs) != 0) {
memset(&rs, 0, sizeof(rs)); memset(&rs, 0, sizeof(rs));
rs.noise = -95; 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(); struct json_object *o = json_object_new_object();
json_object_object_add(o, "name", json_object_object_add(o, "name",
json_object_new_string(radio_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_object_add(o, "channel",
json_object_new_int(rs.channel ? rs.channel : default_ch)); json_object_new_int(rs.channel ? rs.channel : default_ch));
json_object_object_add(o, "tx_power", 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_new_int(rs.num_sta));
json_object_object_add(o, "noise", json_object_object_add(o, "noise",
json_object_new_int(rs.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); json_object_array_add(arr, o);
} }
return arr; 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)) if (json_object_object_get_ex(vap, "handoff_suggestions", &v))
handoff_suggestions = json_object_get_boolean(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 */ /* Map band → wlan interface and current channel */
char wlan_iface[32] = "phy0-ap0"; char wlan_iface[32] = "phy0-ap0";
if (ifname && ifname[0]) 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 */ /* Calculate tx_power of the corresponding radio */
int tx_pwr = 20; int tx_pwr = 20;
radio_stats_t rs2; radio_stats_t rs2;
if (sysinfo_radio(wlan_iface, &rs2) == 0 && rs2.tx_power) if (sysinfo_radio(wlan_iface, &rs2) == 0) {
tx_pwr = rs2.tx_power; 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(); struct json_object *o = json_object_new_object();
json_object_object_add(o, "essid", 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_new_string(vap_name));
json_object_object_add(o, "radio", json_object_object_add(o, "radio",
json_object_new_string(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) if (vlan_id > 0)
json_object_object_add(o, "vlan_id", json_object_new_int(vlan_id)); json_object_object_add(o, "vlan_id", json_object_new_int(vlan_id));
json_object_object_add(o, "up", 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_new_int64(iface_st.rx_dropped));
json_object_object_add(o, "tx_dropped", json_object_object_add(o, "tx_dropped",
json_object_new_int64(iface_st.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. */ /* Only controller-issued ObjectIds are valid in this field. */
if (vap_id) if (vap_id)
json_object_object_add(o, "id", json_object_new_string(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_object_add(o, "usage",
json_object_new_string("user")); json_object_new_string("user"));
json_object_object_add(o, "ccq", 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 */ /* Nested sta_table — clients of THIS VAP */
json_object_object_add(o, "sta_table", sta_tbl); 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_object_add(root, "country_code",
json_object_new_int(0)); json_object_new_int(0));
/* ── CPU + RAM ──────────────────────────────────────────────── */ /* Memory/load and percentage stats use distinct UniFi schemas. */
json_object_object_add(root, "sys_stats", build_sys_stats()); 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 ──────────────── */ /* ── Ethernet interfaces with real counters ──────────────── */
json_object_object_add(root, "if_table", build_if_table(m, st)); json_object_object_add(root, "if_table", build_if_table(m, st));
/* ── Radio capabilities (static, from the model) ─────────────── */ /* Native informs carry RF counters in radio_table[].athstats. Keep the
build_radio_table(root, m); * normalized table too for controller versions that consume it directly. */
struct json_object *radio_stats = build_radio_table_stats(m);
/* ── Real-time channel utilization ────────────────────── */ build_radio_table(root, m, radio_stats);
json_object_object_add(root, "radio_table_stats", json_object_object_add(root, "radio_table_stats", radio_stats);
build_radio_table_stats(m));
/* ── Ethernet ports with actual status ───────────────────────── */ /* ── Ethernet ports with actual status ───────────────────────── */
build_port_table(root, m); build_port_table(root, m);
+266 -18
View File
@@ -38,6 +38,7 @@
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <stdbool.h> #include <stdbool.h>
#include <time.h>
#include <net/if.h> #include <net/if.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <sys/socket.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); &tb,&tp,&te,&td,&tf,&tcol,&tcomp,&tcarr);
out->rx_bytes = rb; out->rx_packets = rp; out->rx_bytes = rb; out->rx_packets = rp;
out->rx_errors = re; out->rx_dropped = rd; out->rx_errors = re; out->rx_dropped = rd;
out->rx_frame = rframe;
out->rx_multicast= rmulti; out->rx_multicast= rmulti;
out->tx_bytes = tb; out->tx_packets = tp; out->tx_bytes = tb; out->tx_packets = tp;
out->tx_errors = te; out->tx_dropped = td; out->tx_errors = te; out->tx_dropped = td;
@@ -213,6 +215,46 @@ int sysinfo_iface(const char *ifname, iface_stats_t *out)
WiFi Radio 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 1. iw dev wlan0 info → channel and power
Example: Example:
Interface wlan0 Interface wlan0
@@ -239,6 +281,8 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
out->noise = -95; out->noise = -95;
char cmd[128]; char cmd[128];
int current_freq = 0;
int wiphy_index = -1;
/* iw dev <iface> info */ /* iw dev <iface> info */
snprintf(cmd, sizeof(cmd), "iw dev %s info 2>/dev/null", iface); 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]; char line[256];
while (fgets(line, sizeof(line), p)) { while (fgets(line, sizeof(line), p)) {
int ch; float mhz; 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; out->channel = ch;
current_freq = (int)(mhz + 0.5f);
}
int index;
if (sscanf(line, " wiphy %d", &index) == 1)
wiphy_index = index;
float tp; float tp;
if (sscanf(line, " txpower %f dBm", &tp) == 1) if (sscanf(line, " txpower %f dBm", &tp) == 1)
out->tx_power = (int)tp; out->tx_power = (int)tp;
} }
pclose(p); 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 */ /* iw dev <iface> survey dump */
snprintf(cmd, sizeof(cmd), "iw dev %s survey dump 2>/dev/null", iface); snprintf(cmd, sizeof(cmd), "iw dev %s survey dump 2>/dev/null", iface);
p = popen(cmd, "r"); p = popen(cmd, "r");
if (!p) return 0; if (!p) return 0;
long long active=0, busy=0, tx_t=0, rx_t=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)) { while (fgets(line, sizeof(line), p)) {
if (strstr(line, "[in use]")) { int survey_freq;
in_use = 1; active=busy=tx_t=rx_t=0; continue; if (sscanf(line, " frequency: %d MHz", &survey_freq) == 1) {
} /* Some drivers omit the optional [in use] marker, especially on
if (!in_use) continue; * the secondary radio. Match the frequency reported by iw info. */
/* New frequency without [in use] resets the block */ selected = strstr(line, "[in use]") != NULL ||
if (strstr(line, "frequency:") && !strstr(line, "[in use]")) { (current_freq > 0 && survey_freq == current_freq);
in_use = 0; continue; if (selected)
active=busy=tx_t=rx_t=0;
continue;
} }
if (!selected) continue;
float noise; long long val; float noise; long long val;
if (sscanf(line, " noise: %f dBm", &noise) == 1) out->noise = (int)noise; if (sscanf(line, " noise: %f dBm", &noise) == 1) out->noise = (int)noise;
if (sscanf(line, " channel active time: %lld ms", &val) == 1) active = val; 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); pclose(p);
if (active > 0) { long long sample_active = active;
out->cu_total = (int)(busy * 100 / active); long long sample_busy = busy;
out->cu_self_tx = (int)(tx_t * 100 / active); long long sample_tx = tx_t;
out->cu_self_rx = (int)(rx_t * 100 / active); 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 */ if (snapshot && active > 0) {
snprintf(cmd, sizeof(cmd), snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
"iw dev %s station dump 2>/dev/null | grep -c '^Station'", snapshot->active = active;
iface); 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"); 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; 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 tx_errors;
long long rx_dropped; long long rx_dropped;
long long tx_dropped; long long tx_dropped;
long long rx_frame;
long long rx_multicast; long long rx_multicast;
} iface_stats_t; } iface_stats_t;
@@ -65,9 +66,33 @@ typedef struct {
int cu_self_rx; /* % time spent receiving */ int cu_self_rx; /* % time spent receiving */
int num_sta; int num_sta;
int noise; /* dBm */ 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; } radio_stats_t;
/* iface: "wlan0", "wlan1" */ /* iface: "wlan0", "wlan1" */
int sysinfo_radio(const char *iface, radio_stats_t *out); 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 */ #endif /* OPENUF_SYSINFO_H */