adding more metric reporting
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user