Files
OpenUniFi/src/inform/payload.c
T
KodaandKoda 4f30a031bb
Build and publish release / metadata (push) Canceled after 0s
Build and publish release / create-release (push) Canceled after 0s
Build and publish release / build (push) Canceled after 0s
Build and publish release / publish-release (push) Canceled after 0s
Generate and publish developer Wiki / generated-wiki (push) Failing after 43s
fixing wiki generation (#38)
Reviewed-on: #38
Co-authored-by: Koda YeenBean <n122330@gmail.com>
2026-07-19 22:34:26 +01:00

802 lines
37 KiB
C

/*
* 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;
}