/* * Collect device, radio, interface, client, and topology telemetry into the * JSON payload sent during an inform exchange. */ #include #include #include #include #include #include #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// 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_ 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//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; }