Reviewed-on: #36 Co-authored-by: Koda YeenBean <n122330@gmail.com>
This commit was merged in pull request #36.
This commit is contained in:
@@ -0,0 +1,802 @@
|
||||
/*
|
||||
* Collect device, radio, interface, client, and topology telemetry into the
|
||||
* JSON payload sent during an inform exchange.
|
||||
*/
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <json-c/json.h>
|
||||
|
||||
#include "inform.h"
|
||||
#include "crypto.h"
|
||||
#include "http.h"
|
||||
#include "wlan.h"
|
||||
#include "state.h"
|
||||
#include "config.h"
|
||||
#include "sysinfo.h"
|
||||
#include "clients.h"
|
||||
#include "lldp.h"
|
||||
#include "inform_internal.h"
|
||||
|
||||
/* Build system CPU, memory, and load statistics. */
|
||||
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) {
|
||||
long used_kb = mem.total_kb - mem.free_kb
|
||||
- mem.buffer_kb - mem.cached_kb;
|
||||
if (used_kb < 0) used_kb = 0;
|
||||
json_object_object_add(o, "mem_total",
|
||||
json_object_new_int64(mem.total_kb * 1024LL));
|
||||
json_object_object_add(o, "mem_used",
|
||||
json_object_new_int64(used_kb * 1024LL));
|
||||
json_object_object_add(o, "mem_buffer",
|
||||
json_object_new_int64(mem.buffer_kb * 1024LL));
|
||||
} else {
|
||||
json_object_object_add(o, "mem_total", json_object_new_int(0));
|
||||
json_object_object_add(o, "mem_used", json_object_new_int(0));
|
||||
json_object_object_add(o, "mem_buffer", json_object_new_int(0));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
if_table — network interface statistics
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
All Ethernet ports on the model are reported.
|
||||
/proc/net/dev is read for counters, and /sys/class/net/<iface>/
|
||||
for speed, duplex, and link status.
|
||||
*/
|
||||
static struct json_object *build_if_table(const uf_model_t *m,
|
||||
const openuf_state_t *st)
|
||||
{
|
||||
struct json_object *arr = json_object_new_array();
|
||||
|
||||
for (int i = 0; i < m->port_table_len; i++) {
|
||||
const char *ifname = m->port_table[i].ifname;
|
||||
iface_stats_t stats;
|
||||
sysinfo_iface(ifname, &stats);
|
||||
|
||||
struct json_object *o = json_object_new_object();
|
||||
json_object_object_add(o, "name",
|
||||
json_object_new_string(ifname));
|
||||
json_object_object_add(o, "mac",
|
||||
json_object_new_string(stats.mac[0] ? stats.mac : st->mac));
|
||||
json_object_object_add(o, "ip",
|
||||
json_object_new_string(stats.ip[0] ? stats.ip : st->ip));
|
||||
json_object_object_add(o, "up",
|
||||
json_object_new_boolean(stats.up));
|
||||
json_object_object_add(o, "speed",
|
||||
json_object_new_int(stats.speed > 0 ? stats.speed : 1000));
|
||||
json_object_object_add(o, "full_duplex",
|
||||
json_object_new_boolean(stats.full_duplex));
|
||||
json_object_object_add(o, "num_port",
|
||||
json_object_new_int(1));
|
||||
json_object_object_add(o, "rx_bytes",
|
||||
json_object_new_int64(stats.rx_bytes));
|
||||
json_object_object_add(o, "tx_bytes",
|
||||
json_object_new_int64(stats.tx_bytes));
|
||||
json_object_object_add(o, "rx_packets",
|
||||
json_object_new_int64(stats.rx_packets));
|
||||
json_object_object_add(o, "tx_packets",
|
||||
json_object_new_int64(stats.tx_packets));
|
||||
json_object_object_add(o, "rx_errors",
|
||||
json_object_new_int64(stats.rx_errors));
|
||||
json_object_object_add(o, "tx_errors",
|
||||
json_object_new_int64(stats.tx_errors));
|
||||
json_object_object_add(o, "rx_dropped",
|
||||
json_object_new_int64(stats.rx_dropped));
|
||||
json_object_object_add(o, "tx_dropped",
|
||||
json_object_new_int64(stats.tx_dropped));
|
||||
json_object_object_add(o, "rx_multicast",
|
||||
json_object_new_int64(stats.rx_multicast));
|
||||
json_object_array_add(arr, o);
|
||||
}
|
||||
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,
|
||||
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(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));
|
||||
json_object_object_add(o, "antenna_gain", json_object_new_int(r->antenna_gain));
|
||||
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 (inform_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);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
radio_table_stats — dynamic channel statistics
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
Channel utilization is read in real time using:
|
||||
iw dev wlan0 survey dump → active/busy/tx/rx time
|
||||
iw dev wlan0 info → current channel, power
|
||||
The controller displays this data in the RF view.
|
||||
*/
|
||||
static struct json_object *build_radio_table_stats(const uf_model_t *m)
|
||||
{
|
||||
struct json_object *arr = json_object_new_array();
|
||||
|
||||
for (int i = 0; i < m->radio_map_len; i++) {
|
||||
const uf_radio_map_t *rm = &m->radio_map[i];
|
||||
char wlan_iface[32];
|
||||
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)
|
||||
? m->radio_table[i].name : wlan_iface;
|
||||
int default_ch = (i < m->radio_table_len)
|
||||
? 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",
|
||||
json_object_new_int(rs.tx_power ? rs.tx_power : default_pwr));
|
||||
json_object_object_add(o, "cu_self_tx",
|
||||
json_object_new_int(rs.cu_self_tx));
|
||||
json_object_object_add(o, "cu_self_rx",
|
||||
json_object_new_int(rs.cu_self_rx));
|
||||
json_object_object_add(o, "cu_total",
|
||||
json_object_new_int(rs.cu_total));
|
||||
json_object_object_add(o, "num_sta",
|
||||
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;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
port_table — Real/actual status of the ethernet ports
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
/sys/class/net/<iface>/speed and operstate are read to
|
||||
reflect the actual status of each port on the controller.
|
||||
*/
|
||||
static void build_port_table(struct json_object *root,
|
||||
const uf_model_t *m)
|
||||
{
|
||||
struct json_object *arr = json_object_new_array();
|
||||
for (int i = 0; i < m->port_table_len; i++) {
|
||||
const uf_port_t *pt = &m->port_table[i];
|
||||
iface_stats_t stats;
|
||||
sysinfo_iface(pt->ifname, &stats);
|
||||
|
||||
struct json_object *o = json_object_new_object();
|
||||
json_object_object_add(o, "ifname",
|
||||
json_object_new_string(pt->ifname));
|
||||
json_object_object_add(o, "name",
|
||||
json_object_new_string(pt->name));
|
||||
json_object_object_add(o, "port_idx",
|
||||
json_object_new_int(pt->port_idx));
|
||||
json_object_object_add(o, "poe_caps",
|
||||
json_object_new_int(pt->poe_caps));
|
||||
json_object_object_add(o, "media",
|
||||
json_object_new_string(pt->media));
|
||||
json_object_object_add(o, "speed",
|
||||
json_object_new_int(stats.speed > 0 ? stats.speed : pt->speed));
|
||||
json_object_object_add(o, "up",
|
||||
json_object_new_boolean(stats.up));
|
||||
json_object_object_add(o, "is_uplink",
|
||||
json_object_new_boolean(pt->is_uplink));
|
||||
json_object_object_add(o, "full_duplex",
|
||||
json_object_new_boolean(stats.full_duplex));
|
||||
json_object_object_add(o, "rx_bytes",
|
||||
json_object_new_int64(stats.rx_bytes));
|
||||
json_object_object_add(o, "tx_bytes",
|
||||
json_object_new_int64(stats.tx_bytes));
|
||||
json_object_array_add(arr, o);
|
||||
}
|
||||
json_object_object_add(root, "port_table", arr);
|
||||
}
|
||||
|
||||
static void build_eth_table(struct json_object *root, const uf_model_t *m)
|
||||
{
|
||||
struct json_object *arr = json_object_new_array();
|
||||
for (int i = 0; i < m->ethernet_table_len; i++) {
|
||||
const uf_eth_entry_t *e = &m->ethernet_table[i];
|
||||
struct json_object *o = json_object_new_object();
|
||||
json_object_object_add(o, "name", json_object_new_string(e->name));
|
||||
json_object_object_add(o, "num_port", json_object_new_int(e->num_port));
|
||||
json_object_array_add(arr, o);
|
||||
}
|
||||
json_object_object_add(root, "ethernet_table", arr);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
vap_table — active VAPs with connected clients (sta_table)
|
||||
═══════════════════════════════════════════════════════════════════
|
||||
For each active VAP in UCI:
|
||||
1. Interface statistics for the wlan are read with sysinfo_iface()
|
||||
2. The current channel is obtained with sysinfo_radio()
|
||||
3. Clients are enumerated with clients_build_sta_table()
|
||||
→ iw dev wlan0 station dump (signal, bitrate, bytes, uptime)
|
||||
→ /proc/net/arp (MAC → IP)
|
||||
→ /tmp/dhcp.leases (MAC → hostname)
|
||||
|
||||
The nested sta_table is what the controller uses to:
|
||||
- Display clients on the dashboard
|
||||
- Calculate per-client statistics
|
||||
- Draw the network topology
|
||||
*/
|
||||
static struct json_object *build_vap_table(const uf_model_t *m)
|
||||
{
|
||||
/* Get list of VAPs from UCI */
|
||||
struct json_object *uci_vaps = wlan_get_vap_table(m);
|
||||
int nvaps = json_object_array_length(uci_vaps);
|
||||
|
||||
struct json_object *arr = json_object_new_array();
|
||||
|
||||
for (int i = 0; i < nvaps; i++) {
|
||||
struct json_object *vap = json_object_array_get_idx(uci_vaps, i);
|
||||
struct json_object *v;
|
||||
|
||||
const char *essid = "";
|
||||
const char *vap_name = "";
|
||||
const char *radio = "ng";
|
||||
const char *bssid = "00:00:00:00:00:00";
|
||||
const char *vap_id = NULL;
|
||||
const char *ifname = NULL;
|
||||
int vlan_id = 0;
|
||||
int is_11r = 0;
|
||||
int band_steering = 0;
|
||||
int handoff_suggestions = 0;
|
||||
|
||||
if (json_object_object_get_ex(vap, "essid", &v)) essid = json_object_get_string(v);
|
||||
if (json_object_object_get_ex(vap, "name", &v)) vap_name = json_object_get_string(v);
|
||||
if (json_object_object_get_ex(vap, "radio", &v)) radio = json_object_get_string(v);
|
||||
if (json_object_object_get_ex(vap, "bssid", &v)) bssid = json_object_get_string(v);
|
||||
if (json_object_object_get_ex(vap, "id", &v)) vap_id = json_object_get_string(v);
|
||||
if (json_object_object_get_ex(vap, "ifname", &v)) ifname = json_object_get_string(v);
|
||||
if (json_object_object_get_ex(vap, "vlan_id", &v)) vlan_id = json_object_get_int(v);
|
||||
if (json_object_object_get_ex(vap, "fast_roaming_enabled", &v))
|
||||
is_11r = json_object_get_boolean(v);
|
||||
if (json_object_object_get_ex(vap, "band_steering", &v))
|
||||
band_steering = json_object_get_boolean(v);
|
||||
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])
|
||||
snprintf(wlan_iface, sizeof(wlan_iface), "%s", ifname);
|
||||
int channel = 6;
|
||||
for (int j = 0; j < m->radio_map_len; j++) {
|
||||
if (strcmp(m->radio_map[j].band, radio) == 0) {
|
||||
int idx = 0;
|
||||
const char *device = wlan_device_for_band(
|
||||
m, m->radio_map[j].band);
|
||||
if (!device) device = m->radio_map[j].device;
|
||||
sscanf(device, "radio%d", &idx);
|
||||
if (!ifname || !ifname[0])
|
||||
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", idx);
|
||||
radio_stats_t rs;
|
||||
if (sysinfo_radio(wlan_iface, &rs) == 0 && rs.channel)
|
||||
channel = rs.channel;
|
||||
else if (idx < m->radio_table_len)
|
||||
channel = m->radio_table[idx].channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wireless interface statistics */
|
||||
iface_stats_t iface_st;
|
||||
sysinfo_iface(wlan_iface, &iface_st);
|
||||
|
||||
/* Clients connected to this VAP */
|
||||
struct json_object *sta_tbl =
|
||||
clients_build_sta_table(wlan_iface, radio, channel, vap_name,
|
||||
vlan_id, is_11r);
|
||||
int num_sta = json_object_array_length(sta_tbl);
|
||||
|
||||
/* Calculate tx_power of the corresponding radio */
|
||||
int tx_pwr = 20;
|
||||
radio_stats_t rs2;
|
||||
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",
|
||||
json_object_new_string(essid));
|
||||
json_object_object_add(o, "bssid",
|
||||
json_object_new_string(bssid));
|
||||
json_object_object_add(o, "name",
|
||||
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",
|
||||
json_object_new_boolean(iface_st.up));
|
||||
json_object_object_add(o, "channel",
|
||||
json_object_new_int(channel));
|
||||
json_object_object_add(o, "tx_power",
|
||||
json_object_new_int(tx_pwr));
|
||||
json_object_object_add(o, "band_steering",
|
||||
json_object_new_boolean(band_steering));
|
||||
json_object_object_add(o, "bss_transition",
|
||||
json_object_new_boolean(handoff_suggestions));
|
||||
json_object_object_add(o, "handoff_suggestions",
|
||||
json_object_new_boolean(handoff_suggestions));
|
||||
json_object_object_add(o, "num_sta",
|
||||
json_object_new_int(num_sta));
|
||||
json_object_object_add(o, "rx_bytes",
|
||||
json_object_new_int64(iface_st.rx_bytes));
|
||||
json_object_object_add(o, "tx_bytes",
|
||||
json_object_new_int64(iface_st.tx_bytes));
|
||||
json_object_object_add(o, "rx_packets",
|
||||
json_object_new_int64(iface_st.rx_packets));
|
||||
json_object_object_add(o, "tx_packets",
|
||||
json_object_new_int64(iface_st.tx_packets));
|
||||
json_object_object_add(o, "rx_errors",
|
||||
json_object_new_int64(iface_st.rx_errors));
|
||||
json_object_object_add(o, "tx_errors",
|
||||
json_object_new_int64(iface_st.tx_errors));
|
||||
json_object_object_add(o, "rx_dropped",
|
||||
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));
|
||||
if (vap_id)
|
||||
json_object_object_add(o, "wlanconf_id",
|
||||
json_object_new_string(vap_id));
|
||||
json_object_object_add(o, "usage",
|
||||
json_object_new_string("user"));
|
||||
json_object_object_add(o, "ccq",
|
||||
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);
|
||||
|
||||
json_object_array_add(arr, o);
|
||||
}
|
||||
json_object_put(uci_vaps);
|
||||
return arr;
|
||||
}
|
||||
|
||||
/* Build the device-level station table UniFi uses for client ownership. */
|
||||
static struct json_object *collect_sta_table(struct json_object *vap_table)
|
||||
{
|
||||
struct json_object *all = json_object_new_array();
|
||||
int vap_count = json_object_array_length(vap_table);
|
||||
for (int i = 0; i < vap_count; i++) {
|
||||
struct json_object *vap = json_object_array_get_idx(vap_table, i);
|
||||
struct json_object *stations;
|
||||
if (!json_object_object_get_ex(vap, "sta_table", &stations) ||
|
||||
!json_object_is_type(stations, json_type_array))
|
||||
continue;
|
||||
int count = json_object_array_length(stations);
|
||||
for (int j = 0; j < count; j++)
|
||||
json_object_array_add(all, json_object_get(
|
||||
json_object_array_get_idx(stations, j)));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
inform_build_payload — Complete assembly of the inform JSON
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
char *inform_build_payload(const openuf_state_t *st,
|
||||
const uf_model_t *m,
|
||||
long uptime)
|
||||
{
|
||||
/* MAC without colons → serial (uppercase) */
|
||||
char mac_clean[32] = {0};
|
||||
{
|
||||
const char *s = st->mac; int j = 0;
|
||||
for (int i = 0; s[i] && j < 12; i++)
|
||||
if (s[i] != ':') {
|
||||
char c = s[i];
|
||||
if (c >= 'a' && c <= 'f') c -= 32;
|
||||
mac_clean[j++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
char fw_version[64];
|
||||
if (st->firmware_version[0])
|
||||
snprintf(fw_version, sizeof(fw_version), "%s",
|
||||
st->firmware_version);
|
||||
else
|
||||
snprintf(fw_version, sizeof(fw_version), "%s%s",
|
||||
m->fw_pre, m->fw_ver);
|
||||
|
||||
char inform_url_buf[256];
|
||||
if (st->inform_url[0])
|
||||
strncpy(inform_url_buf, st->inform_url, sizeof(inform_url_buf)-1);
|
||||
else
|
||||
snprintf(inform_url_buf, sizeof(inform_url_buf),
|
||||
"http://unifi:%d%s", INFORM_PORT, INFORM_PATH);
|
||||
|
||||
struct json_object *root = json_object_new_object();
|
||||
|
||||
/* ── Device identity ──────────────────────────────── */
|
||||
json_object_object_add(root, "mac",
|
||||
json_object_new_string(st->mac));
|
||||
json_object_object_add(root, "serial",
|
||||
json_object_new_string(mac_clean));
|
||||
json_object_object_add(root, "model",
|
||||
json_object_new_string(m->model));
|
||||
json_object_object_add(root, "model_display",
|
||||
json_object_new_string(m->model_display));
|
||||
json_object_object_add(root, "display_name",
|
||||
json_object_new_string(m->display_name));
|
||||
json_object_object_add(root, "board_rev",
|
||||
json_object_new_int(m->board_rev));
|
||||
json_object_object_add(root, "version",
|
||||
json_object_new_string(fw_version));
|
||||
json_object_object_add(root, "bootrom_version",
|
||||
json_object_new_string("openuf-v0.4"));
|
||||
json_object_object_add(root, "required_version",
|
||||
json_object_new_string("9.0.114"));
|
||||
json_object_object_add(root, "ip",
|
||||
json_object_new_string(st->ip));
|
||||
json_object_object_add(root, "hostname",
|
||||
json_object_new_string(st->hostname[0] ? st->hostname : m->display_name));
|
||||
json_object_object_add(root, "inform_url",
|
||||
json_object_new_string(inform_url_buf));
|
||||
json_object_object_add(root, "uptime",
|
||||
json_object_new_int64(uptime));
|
||||
json_object_object_add(root, "time",
|
||||
json_object_new_int64((long long)uptime));
|
||||
json_object_object_add(root, "state",
|
||||
json_object_new_int(st->adopted ? 4 : 1));
|
||||
json_object_object_add(root, "default",
|
||||
json_object_new_boolean(!st->adopted));
|
||||
json_object_object_add(root, "cfgversion",
|
||||
json_object_new_string(st->cfgversion));
|
||||
json_object_object_add(root, "x_authkey",
|
||||
json_object_new_string(st->adopted ? st->authkey : DEFAULT_AUTH_KEY));
|
||||
json_object_object_add(root, "_default_key",
|
||||
json_object_new_boolean(!st->adopted));
|
||||
json_object_object_add(root, "has_eth1",
|
||||
json_object_new_boolean(m->has_eth1));
|
||||
json_object_object_add(root, "isolated",
|
||||
json_object_new_boolean(false));
|
||||
json_object_object_add(root, "locating",
|
||||
json_object_new_boolean(false));
|
||||
json_object_object_add(root, "uplink",
|
||||
json_object_new_string("eth0"));
|
||||
json_object_object_add(root, "country_code",
|
||||
json_object_new_int(0));
|
||||
|
||||
/* 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));
|
||||
|
||||
/* 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);
|
||||
build_eth_table(root, m);
|
||||
|
||||
/* Publish both per-VAP and device-level station views. */
|
||||
struct json_object *vap_table = build_vap_table(m);
|
||||
struct json_object *sta_table = collect_sta_table(vap_table);
|
||||
int station_count = json_object_array_length(sta_table);
|
||||
json_object_object_add(root, "vap_table", vap_table);
|
||||
json_object_object_add(root, "sta_table", sta_table);
|
||||
|
||||
/* ── LLDP neighbors for visual topology ─────────────────────── */
|
||||
json_object_object_add(root, "lldp_table", lldp_read_neighbors());
|
||||
|
||||
/* Global counters */
|
||||
json_object_object_add(root, "bytes_r", json_object_new_int(0));
|
||||
json_object_object_add(root, "bytes_d", json_object_new_int(0));
|
||||
json_object_object_add(root, "num_sta", json_object_new_int(station_count));
|
||||
|
||||
const char *s = json_object_to_json_string(root);
|
||||
|
||||
/* Log shows what authkey is actually in the payload */
|
||||
LOG("Payload state=%d, default=%s, adopted=%d, cfgversion=%s, config_applied=%d, x_authkey=%.8s...",
|
||||
st->adopted ? 4 : 1,
|
||||
!st->adopted ? "true" : "false",
|
||||
st->adopted,
|
||||
st->cfgversion,
|
||||
st->config_applied,
|
||||
st->authkey[0] ? st->authkey : "DEFAULT");
|
||||
|
||||
char *copy = strdup(s);
|
||||
json_object_put(root);
|
||||
return copy;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user