diff --git a/src/clients.c b/src/clients.c index 3505d2c..fcfdc64 100644 --- a/src/clients.c +++ b/src/clients.c @@ -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)); diff --git a/src/clients.h b/src/clients.h index 338831f..bf1382c 100644 --- a/src/clients.h +++ b/src/clients.h @@ -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; diff --git a/src/inform.c b/src/inform.c index 443680e..a4350af 100644 --- a/src/inform.c +++ b/src/inform.c @@ -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_ 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); diff --git a/src/sysinfo.c b/src/sysinfo.c index aee917a..347ac8f 100644 --- a/src/sysinfo.c +++ b/src/sysinfo.c @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -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 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 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; +} diff --git a/src/sysinfo.h b/src/sysinfo.h index 983d2dc..2172a3f 100644 --- a/src/sysinfo.h +++ b/src/sysinfo.h @@ -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 */