Translated all files to ENG (#5)

Reviewed-on: #5
Co-authored-by: Finn <fwaggoner@nmfpgt.de>
Co-committed-by: Finn <fwaggoner@nmfpgt.de>
This commit was merged in pull request #5.
This commit is contained in:
2026-07-12 01:45:38 +01:00
committed by Koda
parent fb542777ca
commit 59274da4e5
14 changed files with 317 additions and 307 deletions
+4 -4
View File
@@ -1,11 +1,11 @@
# openuf — Makefile para compilar directamente en el dispositivo
#
# Requisitos:
# opkg install gcc make \
# Requirements:
# apk add gcc make \
# libmbedtls-dev libuci-dev libjson-c-dev \
# lldpd (opcional, para leer vecinos LLDP)
# lldpd (optional for LLDP neighbor discovery, UniFi tree view)
#
# Uso:
# Use:
# make -f Makefile.standalone
# make -f Makefile.standalone install
+1
View File
@@ -81,6 +81,7 @@ The process is automatic:
2. Click on "Adopt" → the controller sends a new key.
3. The AP applies the key and becomes "Connected".
4. The controller pushes the WiFi configuration (SSIDs, channels, etc.).
5. Make sure your WiFi Name doesn't have characters like " - " cause of bad JSON! Grrr
To reset: `rm /etc/openuf/state.json && reboot`
+2
View File
@@ -0,0 +1,2 @@
TNBU - TNBU is the magic string/identifier at the start of the binary packet format used in this custom Inform protocol implementation.
CCQ Client Connection Quality
+19 -19
View File
@@ -1,26 +1,26 @@
/*
* openuf - announce.c
*
* Implementa el protocolo de descubrimiento UDP de UniFi (puerto 10001).
* Implements the UniFi UDP discovery protocol (port 10001).
*
* ── Destinos ─────────────────────────────────────────────────────────
* El protocolo especifica que los paquetes de anuncio se envían a DOS destinos:
* ── Discovery Targets ────────────────────────────────────────────────
* The protocol specifies that discovery packets are sent to TWO targets:
* 1. Broadcast: 255.255.255.255:10001
* 2. Multicast: 233.89.188.1:10001 ← requerido para redes con multicast
* 2. Multicast: 233.89.188.1:10001 ← required on multicast-enabled networks
*
* El controlador UniFi escucha en ambas direcciones.
* Usar sólo broadcast puede fallar en redes donde el broadcast está filtrado.
* The UniFi controller listens on both addresses.
* Using broadcast alone may fail on networks where broadcast traffic is filtered.
*
* ── Formato del paquete ──────────────────────────────────────────────
* Header: [0x02][0x06][0x00][total_payload_len] (4 bytes fijos)
* ── Packet Format ────────────────────────────────────────────────────
* Header: [0x02][0x06][0x00][total_payload_len] (fixed 4-byte header)
* TLVs: [type:1][len_hi:1][len_lo:1][value:len]
*
* ── Modelo U6 InWall ─────────────────────────────────────────────────
* Se emula este modelo específicamente porque:
* - Tiene 5 puertos GbE (eth0-eth4): cubre la mayoría de routers OpenWrt
* - Soporta WiFi 6 (802.11ax) en 2.4 GHz y 5 GHz
* - Tiene PoE passthrough (útil para redes de campus)
* - Es un modelo actual y bien soportado por el controlador
* ── U6 In-Wall Model ─────────────────────────────────────────────────
* This model is emulated because:
* - It provides 5 Gigabit Ethernet ports (eth0-eth4), covering most OpenWrt routers
* - Supports WiFi 6 (802.11ax) on both 2.4 GHz and 5 GHz bands
* - Includes PoE passthrough, useful for campus and enterprise deployments
* - Is a current model with excellent UniFi Controller compatibility
*/
#include <stdio.h>
@@ -212,9 +212,9 @@ int announce_init(announce_ctx_t *ctx,
};
bind(ctx->sockfd, (struct sockaddr *)&bind_addr, sizeof(bind_addr));
/* ── Socket para multicast 233.89.188.1 ────────────────────── */
/* El controlador UniFi también escucha en este grupo multicast.
* Esto es necesario cuando broadcast está filtrado en la red. */
/* ── Multicast Socket (233.89.188.1) ────────────────────────── */
/* The UniFi Controller also listens on this multicast group,
* allowing discovery even when broadcast traffic is filtered. */
ctx->sockfd_mcast = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (ctx->sockfd_mcast >= 0) {
int ttl = 1; /* TTL=1: no cruzar router */
@@ -241,7 +241,7 @@ int announce_send(announce_ctx_t *ctx)
int ret = 0;
/* ── Envío 1: Broadcast 255.255.255.255:10001 ─────────────── */
/* ── Sending 1: Broadcast 255.255.255.255:10001 ─────────────── */
struct sockaddr_in dest_bcast = {
.sin_family = AF_INET,
.sin_port = htons(ANNOUNCE_PORT),
@@ -253,7 +253,7 @@ int announce_send(announce_ctx_t *ctx)
ret = -1;
}
/* ── Envío 2: Multicast 233.89.188.1:10001 ────────────────── */
/* ── Sending 2: Multicast 233.89.188.1:10001 ────────────────── */
if (ctx->sockfd_mcast >= 0) {
struct sockaddr_in dest_mcast = {
.sin_family = AF_INET,
+25 -22
View File
@@ -1,11 +1,13 @@
/*
* openuf - clients.c
*
* Enumera clientes para el payload inform → sta_table.
*
* Enumerates clients for the inform payload → sta_table.
*
* ── Parseo de iw dev station dump ───────────────────────────────────
* ── Parsing `iw dev station dump` Output ────────────────────────────
*
* The output is organized into one block per client:
*
* La salida tiene bloques por cliente:
*
* Station aa:bb:cc:dd:ee:ff (on wlan0)
* inactive time: 120 ms
@@ -18,16 +20,16 @@
* rx bitrate: 108.0 MBit/s
* connected time: 1800 seconds
*
* Detectamos el inicio de cada cliente con "Station XX:XX:..." y
* rellenamos los campos hasta encontrar el siguiente cliente.
* We detect the start of each client by looking for "Station XX:XX:..."
* and populate its fields until the next client entry is encountered.
*
* ── ARP: /proc/net/arp ─────────────────────────────────────────────
*
* IP HW type Flags HW addr Mask Device
* 192.168.1.x 0x1 0x2 aa:bb:cc:dd:ee:ff * br-lan
*
* Flags 0x2 = entrada completa (reachable).
* Flags 0x0 = incompleta (no responde ARP), ignorar.
* Flags 0x2 = complete entry (reachable).
* Flags 0x0 = incomplete (no ARP responce), ignore.
*/
#define _GNU_SOURCE
@@ -39,7 +41,7 @@
#include "clients.h"
/* ─── Normalizar MAC a minúsculas ─────────────────────────────────── */
/* ─── Convert MAC address to lowercase ─────────────────────────────────── */
static void mac_lower(const char *src, char *dst, size_t sz)
{
for (size_t i = 0; src[i] && i < sz-1; i++)
@@ -114,7 +116,7 @@ int clients_mac_to_hostname(const char *mac, char *out, size_t sz)
return -1;
}
/* ─── Parsear tasa de bits "144.4 MBit/s ..." → kbps ───────────── */
/* ─── Parse bitrate "144.4 MBit/s ..." → kbps ───────────── */
static long parse_rate_kbps(const char *s)
{
float r = 0;
@@ -144,7 +146,7 @@ int clients_read_wifi(const char *wlan_iface,
while (fgets(line, sizeof(line), p)) {
line[strcspn(line, "\r\n")] = '\0';
/* ── Nueva estación ──────────────────────────────────────── */
/* ── New station ──────────────────────────────────────── */
char mac[32], on_iface[32];
if (sscanf(line, "Station %31s (on %31[^)])", mac, on_iface) == 2) {
if (count >= max_out) break;
@@ -159,14 +161,14 @@ int clients_read_wifi(const char *wlan_iface,
}
if (!cur) continue;
/* ── Contadores ──────────────────────────────────────────── */
/* ── Counters ──────────────────────────────────────────── */
long long llv;
if (sscanf(line, " rx bytes: %lld", &llv) == 1) { cur->rx_bytes = llv; continue; }
if (sscanf(line, " tx bytes: %lld", &llv) == 1) { cur->tx_bytes = llv; continue; }
if (sscanf(line, " rx packets: %lld", &llv) == 1) { cur->rx_packets = llv; continue; }
if (sscanf(line, " tx packets: %lld", &llv) == 1) { cur->tx_packets = llv; continue; }
/* ── Sal ───────────────────────────────────────────────── */
/* ── Signal ───────────────────────────────────────────────── */
int sig;
if (sscanf(line, " signal: %d", &sig) == 1) { cur->signal = sig; continue; }
@@ -179,7 +181,7 @@ int clients_read_wifi(const char *wlan_iface,
cur->rx_rate = parse_rate_kbps(rest); continue;
}
/* ── Tiempo conectado ────────────────────────────────────── */
/* ── Connection time ────────────────────────────────────── */
int upt;
if (sscanf(line, " connected time: %d seconds", &upt) == 1) {
cur->uptime = upt; continue;
@@ -187,7 +189,7 @@ int clients_read_wifi(const char *wlan_iface,
}
pclose(p);
/* ── Enriquecer: IP, hostname, rssi, CCQ ─────────────────────── */
/* ── Enrich with IP address, hostname, RSSI, and CCQ.─────────────────────── */
for (int i = 0; i < count; i++) {
sta_info_t *s = &out[i];
clients_mac_to_ip(s->mac, s->ip, sizeof(s->ip));
@@ -195,14 +197,15 @@ int clients_read_wifi(const char *wlan_iface,
if (!s->hostname[0])
strncpy(s->hostname, s->mac, sizeof(s->hostname)-1);
/* RSN = SNR estimado (signal - noise) */
/* Estimated SNR (signal - noise). */
s->rssi = s->signal - s->noise;
if (s->rssi < 0) s->rssi = 0;
/* CCQ: métrica 0-1000
* -50 dBm → 1000 (excelente)
* -90 dBm → 0 (muy malo)
* fórmula lineal: (signal + 90) * 25, limitado 0-1000 */
/* CCQ: 0-1000 quality metric
* -50 dBm → 1000 (excellent)
* -90 dBm → 0 (very poor)
* Linear mapping: (signal + 90) * 25, clamped to the range 0-1000.
*/
int ccq = (s->signal + 90) * 25;
s->ccq = (ccq < 0) ? 0 : (ccq > 1000) ? 1000 : ccq;
}
@@ -210,11 +213,11 @@ int clients_read_wifi(const char *wlan_iface,
}
/* ═══════════════════════════════════════════════════════════════════
Construir JSON sta_table para un VAP
Build the sta_table JSON array for a VAP.
═══════════════════════════════════════════════════════════════════
El JSON array resultante se anida dentro de vap_table[i].sta_table
en el payload inform. Ejemplo de entrada:
The resulting JSON array is embedded in vap_table[i].sta_table
within the inform payload. Example input:
{
"mac": "aa:bb:cc:dd:ee:ff",
"ip": "192.168.1.100",
+25 -21
View File
@@ -4,31 +4,35 @@
/*
* openuf - clients.h
*
* Enumera clientes conectados (WiFi y ethernet) para el sta_table
* del payload inform.
* Enumerates connected clients (Wi-Fi and Ethernet) for the
* sta_table in the inform payload.
*
* ── WiFi: iw dev <iface> station dump ───────────────────────────
* ── Wi-Fi: iw dev <iface> station dump ───────────────────────────
*
* Por cada cliente asociado devuelve:
* MAC, señal (dBm), tx/rx bitrate (MBit/s), tx/rx bytes,
* tx/rx packets, connected time (segundos)
* Returns the following information for each associated client:
* MAC address, signal strength (dBm), TX/RX bitrate (Mbit/s),
* TX/RX bytes, TX/RX packets, and connected time (seconds).
*
* ── IP del cliente: /proc/net/arp ───────────────────────────────
* ── Client IP Address: /proc/net/arp ─────────────────────────────
*
* Cruce MAC → IP. Solo entradas completas (flags=0x2).
* Maps MAC addresses to IP addresses. Only complete entries
* (flags = 0x2) are used.
*
* ── Hostname: /tmp/dhcp.leases (dnsmasq) ────────────────────────
* ── Hostname: /tmp/dhcp.leases (dnsmasq) ────────────────────────
*
* Formato: timestamp MAC IP hostname client-id
* Format: timestamp MAC IP hostname client-id
*
* ── Ethernet: bridge fdb show ───────────────────────────────────
*
* MACs dinámicas (no permanent, no multicast) en el bridge.
* Discovers dynamic MAC addresses (excluding permanent and
* multicast entries) in the bridge forwarding database.
*
* ── CCQ (Client Connection Quality) ─────────────────────────────
* ── CCQ (Client Connection Quality) ─────────────────────────────
*
* Estimated 01000 quality metric derived from RSSI. The
* UniFi Controller displays it as the client's signal quality
* indicator.
*
* Métrica 0-1000 basada en RSSI. El controlador la muestra
* como barra de calidad de señal del cliente.
* CCQ = clamp((signal + 90) * 25, 0, 1000)
*/
@@ -42,7 +46,7 @@ typedef struct {
char mac[32];
char ip[64];
char hostname[64];
int signal; /* RSSI dBm (negativo) */
int signal; /* RSSI dBm (negative) */
int noise; /* dBm */
int rssi; /* SNR ≈ signal - noise */
long tx_rate; /* kbps */
@@ -51,7 +55,7 @@ typedef struct {
long long rx_bytes;
long long tx_packets;
long long rx_packets;
int uptime; /* segundos conectado */
int uptime; /* seconds online */
char radio[8]; /* "ng" / "na" / "6g" */
int channel;
char vap_name[32];
@@ -60,15 +64,15 @@ typedef struct {
bool is_wired;
} sta_info_t;
/* Lee clientes WiFi de una interfaz. Devuelve nº de clientes. */
/* Reads Wi-Fi clients from an interface. Returns the number of clients. */
int clients_read_wifi(const char *wlan_iface,
const char *radio_band,
int channel,
sta_info_t *out,
int max_out);
/* Construye JSON array sta_table para un VAP.
* El caller debe liberar con json_object_put(). */
/* Creates a JSON array `sta_table` for a VAP.
* The caller must free it using `json_object_put()` */
struct json_object *clients_build_sta_table(const char *wlan_iface,
const char *radio_band,
int channel,
@@ -76,10 +80,10 @@ struct json_object *clients_build_sta_table(const char *wlan_iface,
int vlan_id,
int is_11r);
/* Busca IP en /proc/net/arp dado un MAC. */
/* Look up the IP address in /proc/net/arp given a MAC address. */
int clients_mac_to_ip(const char *mac, char *ip_out, size_t sz);
/* Busca hostname en /tmp/dhcp.leases dado un MAC. */
/* Find the hostname in /tmp/dhcp.leases given a MAC address */
int clients_mac_to_hostname(const char *mac, char *out, size_t sz);
#endif /* OPENUF_CLIENTS_H */
+89 -89
View File
@@ -1,50 +1,50 @@
/*
* openuf - inform.c
*
* Protocolo Inform de UniFi — implementación completa.
* UniFi Inform Protocol — full implementation.
*
* ── CÓMO FUNCIONA ────────────────────────────────────────────────────
* ── HOW IT WORKS ─────────────────────────────────────────────────────
*
* Cada 10 segundos el AP hace HTTP POST a http://<controller>:8080/inform
* con un paquete binario TNBU que contiene JSON cifrado con AES-128-CBC.
* Every 10 seconds the AP makes an HTTP POST to http://<controller>:8080/inform
* with a binary TNBU packet containing JSON encrypted with AES-128-CBC.
*
* El controlador responde con otro paquete TNBU. El AP descifra, parsea
* el JSON y ejecuta la acción (_type).
* The controller responds with another TNBU packet. The AP decrypts, parses
* the JSON, and executes the action (_type).
*
* ── PAQUETE BINARIO TNBU ─────────────────────────────────────────────
* ── TNBU BINARY PACKET ───────────────────────────────────────────────
*
* Offset Bytes Campo
* Offset Bytes Field
* ------ ----- -----
* 0 4 Magic "TNBU"
* 4 4 Versión paquete (=0), uint32 BE
* 8 6 MAC del AP
* 14 2 Flags: bit0=cifrado, bit1=zlib
* 16 16 IV de AES (cuando cifrado)
* 32 4 Versión de datos (=1), uint32 BE
* 36 4 Longitud del payload, uint32 BE
* 40 N Payload JSON, cifrado con AES-128-CBC
* 4 4 Packet version (=0), uint32 BE
* 8 6 AP MAC address
* 14 2 Flags: bit0=encrypted, bit1=zlib
* 16 16 AES IV (when encrypted)
* 32 4 Data version (=1), uint32 BE
* 36 4 Payload length, uint32 BE
* 40 N JSON payload, encrypted with AES-128-CBC
*
* ── CÓMO SE LEEN LOS PARÁMETROS ──────────────────────────────────────
* ── HOW PARAMETERS ARE READ ──────────────────────────────────────────
*
* CPU: sysinfo_cpu_percent() → /proc/stat (delta 2 llamadas)
* RAM: sysinfo_mem() → /proc/meminfo
* Interfaces: sysinfo_iface() → /proc/net/dev + /sys/class/net/
* Radios: sysinfo_radio() → iw dev <iface> info + survey
* VAPs UCI: wlan_get_vap_table() → libuci wireless.*
* Clientes WiFi: clients_build_sta_table() → iw dev <iface> station dump
* Clientes IP: clients_mac_to_ip() → /proc/net/arp
* Clientes nombre: clients_mac_to_hostname() → /tmp/dhcp.leases
* LLDP vecinos: lldp_read_neighbors() → lldpctl -f json
* CPU: sysinfo_cpu_percent() → /proc/stat (delta across 2 calls)
* RAM: sysinfo_mem() → /proc/meminfo
* Interfaces: sysinfo_iface() → /proc/net/dev + /sys/class/net/
* Radios: sysinfo_radio() → iw dev <iface> info + survey
* UCI VAPs: wlan_get_vap_table() → libuci wireless.*
* WiFi clients: clients_build_sta_table() → iw dev <iface> station dump
* IP clients: clients_mac_to_ip() → /proc/net/arp
* Client names: clients_mac_to_hostname() → /tmp/dhcp.leases
* LLDP neighbors: lldp_read_neighbors() → lldpctl -f json
*
* ── CICLO DE ADOPCIÓN ────────────────────────────────────────────────
* ── ADOPTION CYCLE ───────────────────────────────────────────────────
*
* 1. AP envía inform con key=DEFAULT, default=true, state=1
* 2. Controller responde: {_type:"cmd", cmd:"set-adopt",
* key:"nuevaclave32hex", uri:"http://..."}
* 3. AP guarda nueva clave + URL en state.json, adopted=true
* 4. AP envía inform con nueva clave, state=4, default=false
* 5. Controller responde: {_type:"setstate", radio_table:[...], vap_table:[...]}
* 6. AP aplica config WiFi via wlan_apply_config() → libuci → wifi reload
* 1. AP sends inform with key=DEFAULT, default=true, state=1
* 2. Controller responds: {_type:"cmd", cmd:"set-adopt",
* key:"new32hexkey", uri:"http://..."}
* 3. AP saves the new key + URL to state.json, adopted=true
* 4. AP sends inform with the new key, state=4, default=false
* 5. Controller responds: {_type:"setstate", radio_table:[...], vap_table:[...]}
* 6. AP applies WiFi config via wlan_apply_config() → libuci → wifi reload
*/
#include <stdio.h>
@@ -95,10 +95,10 @@ static int valid_authkey(const char *key)
}
/* ═══════════════════════════════════════════════════════════════════
sys_stats — CPU y memoria del sistema
sys_stats — CPU and memory of the system
═══════════════════════════════════════════════════════════════════
El controlador muestra CPU y RAM en la vista del dispositivo.
Leemos /proc/stat y /proc/meminfo directamente.
The controller shows CPU and RAM in the device view.
We read /proc/stat and /proc/meminfo directly.
*/
static struct json_object *build_sys_stats(void)
{
@@ -121,7 +121,7 @@ static struct json_object *build_sys_stats(void)
json_object_object_add(o, "mem_buffer", json_object_new_int(0));
}
/* CPU — delta respecto a llamada anterior (cada ~10s da buen promedio) */
/* CPU — delta relative to the previous call (every ~10s gives a good average) */
json_object_object_add(o, "cpu",
json_object_new_int(sysinfo_cpu_percent()));
@@ -129,11 +129,11 @@ static struct json_object *build_sys_stats(void)
}
/* ═══════════════════════════════════════════════════════════════════
if_table — estadísticas de interfaces de red
if_table — network interface statistics
═══════════════════════════════════════════════════════════════════
Reportamos todos los puertos ethernet del modelo.
Leemos /proc/net/dev para contadores y /sys/class/net/<iface>/
para velocidad, duplex y estado del enlace.
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)
@@ -184,10 +184,10 @@ static struct json_object *build_if_table(const uf_model_t *m,
}
/* ═══════════════════════════════════════════════════════════════════
radio_table — definición estática del hardware de radio
radio_table — static definition of the radio hardware
═══════════════════════════════════════════════════════════════════
Describe las capacidades físicas de cada radio al controlador.
El controlador usa esto para saber qué frecuencias y modos soporta.
Describes the physical capabilities of each radio to the controller.
The controller uses this to know which frequencies and modes it supports.
*/
static void build_radio_table(struct json_object *root,
const uf_model_t *m)
@@ -215,12 +215,12 @@ static void build_radio_table(struct json_object *root,
}
/* ═══════════════════════════════════════════════════════════════════
radio_table_stats — estadísticas dinámicas de canal
radio_table_stats — dynamic channel statistics
═══════════════════════════════════════════════════════════════════
Leemos en tiempo real la utilización del canal con:
iw dev wlan0 survey dump → active/busy/tx/rx time
iw dev wlan0 info → canal actual, potencia
El controlador muestra estos datos en la vista de RF.
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)
{
@@ -229,13 +229,13 @@ static struct json_object *build_radio_table_stats(const uf_model_t *m)
for (int i = 0; i < m->radio_map_len; i++) {
const uf_radio_map_t *rm = &m->radio_map[i];
/* Mapear "radio0" → "wlan0" por convención OpenWrt */
/* Map "radio0" → "wlan0" by OpenWrt convention */
char wlan_iface[32];
int ridx = 0;
sscanf(rm->device, "radio%d", &ridx);
snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx);
/* Nombre del radio en la tabla estática */
/* 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)
@@ -272,10 +272,10 @@ static struct json_object *build_radio_table_stats(const uf_model_t *m)
}
/* ═══════════════════════════════════════════════════════════════════
port_table — estado real de los puertos ethernet
port_table — Real/actual status of the ethernet ports
═══════════════════════════════════════════════════════════════════
Leemos /sys/class/net/<iface>/speed y operstate para
reflejar el estado real de cada puerto en el controlador.
/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)
@@ -328,24 +328,24 @@ static void build_eth_table(struct json_object *root, const uf_model_t *m)
}
/* ═══════════════════════════════════════════════════════════════════
vap_table — VAPs activas con clientes conectados (sta_table)
vap_table — active VAPs with connected clients (sta_table)
═══════════════════════════════════════════════════════════════════
Para cada VAP activa en UCI:
1. Leemos estasticas de la interfaz wlan con sysinfo_iface()
2. Obtenemos el canal actual con sysinfo_radio()
3. Enumeramos clientes con clients_build_sta_table()
→ iw dev wlan0 station dump (sal, bitrate, bytes, uptime)
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)
El sta_table anidado es lo que el controlador usa para:
- Mostrar clientes en el dashboard
- Calcular estadísticas por cliente
- Dibujar la topología de la red
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)
{
/* Obtener lista de VAPs desde UCI */
/* Get list of VAPs from UCI */
struct json_object *uci_vaps = wlan_get_vap_table(m);
int nvaps = json_object_array_length(uci_vaps);
@@ -374,7 +374,7 @@ static struct json_object *build_vap_table(const uf_model_t *m)
if (json_object_object_get_ex(vap, "fast_roaming_enabled", &v))
is_11r = json_object_get_boolean(v);
/* Mapear banda → interfaz wlan y canal actual */
/* 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);
@@ -394,17 +394,17 @@ static struct json_object *build_vap_table(const uf_model_t *m)
}
}
/* Estadísticas de la interfaz inalámbrica */
/* Wireless interface statistics */
iface_stats_t iface_st;
sysinfo_iface(wlan_iface, &iface_st);
/* Clientes conectados a esta VAP */
/* 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);
/* Calcular tx_power del radio correspondiente */
/* Calculate tx_power of the corresponding radio */
int tx_pwr = 20;
radio_stats_t rs2;
if (sysinfo_radio(wlan_iface, &rs2) == 0 && rs2.tx_power)
@@ -452,7 +452,7 @@ static struct json_object *build_vap_table(const uf_model_t *m)
json_object_new_string("user"));
json_object_object_add(o, "ccq",
json_object_new_int(0));
/* sta_table anidado — clientes de ESTA VAP */
/* Nested sta_table — clients of THIS VAP */
json_object_object_add(o, "sta_table", sta_tbl);
json_object_array_add(arr, o);
@@ -481,13 +481,13 @@ static struct json_object *collect_sta_table(struct json_object *vap_table)
}
/* ═══════════════════════════════════════════════════════════════════
build_payload — ensamblado completo del JSON inform
build_payload — Complete assembly of the inform JSON
═══════════════════════════════════════════════════════════════════ */
static char *build_payload(const openuf_state_t *st,
const uf_model_t *m,
long uptime)
{
/* MAC sin colones → serial (uppercase) */
/* MAC without colons → serial (uppercase) */
char mac_clean[32] = {0};
{
const char *s = st->mac; int j = 0;
@@ -511,7 +511,7 @@ static char *build_payload(const openuf_state_t *st,
struct json_object *root = json_object_new_object();
/* ── Identidad del dispositivo ──────────────────────────────── */
/* ── Device identity ──────────────────────────────── */
json_object_object_add(root, "mac",
json_object_new_string(st->mac));
json_object_object_add(root, "serial",
@@ -564,17 +564,17 @@ static char *build_payload(const openuf_state_t *st,
/* ── CPU + RAM ──────────────────────────────────────────────── */
json_object_object_add(root, "sys_stats", build_sys_stats());
/* ── Interfaces ethernet con contadores reales ──────────────── */
/* ── Ethernet interfaces with real counters ──────────────── */
json_object_object_add(root, "if_table", build_if_table(m, st));
/* ── Capacidades de radio (estático del modelo) ─────────────── */
/* ── Radio capabilities (static, from the model) ─────────────── */
build_radio_table(root, m);
/* ── Utilización de canal en tiempo real ────────────────────── */
/* ── Real-time channel utilization ────────────────────── */
json_object_object_add(root, "radio_table_stats",
build_radio_table_stats(m));
/* ── Puertos ethernet con estado real ───────────────────────── */
/* ── Ethernet ports with actual status ───────────────────────── */
build_port_table(root, m);
build_eth_table(root, m);
@@ -585,10 +585,10 @@ static char *build_payload(const openuf_state_t *st,
json_object_object_add(root, "vap_table", vap_table);
json_object_object_add(root, "sta_table", sta_table);
/* ── Vecinos LLDP para topología visual ─────────────────────── */
/* ── LLDP neighbors for visual topology ─────────────────────── */
json_object_object_add(root, "lldp_table", lldp_read_neighbors());
/* Contadores globales */
/* 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));
@@ -610,7 +610,7 @@ static char *build_payload(const openuf_state_t *st,
}
/* ═══════════════════════════════════════════════════════════════════
Paquete binario TNBU
TNBU binary packet
═══════════════════════════════════════════════════════════════════ */
static unsigned char *build_packet(const char *mac_hex,
const char *key_hex,
@@ -668,7 +668,7 @@ static unsigned char *build_packet(const char *mac_hex,
}
/* ═══════════════════════════════════════════════════════════════════
Parsear respuesta binaria del controlador
Parse binary response from the controller
═══════════════════════════════════════════════════════════════════ */
static char *parse_packet(const unsigned char *data, size_t data_len,
const char *key_hex)
@@ -717,13 +717,13 @@ static char *parse_packet(const unsigned char *data, size_t data_len,
}
/* ═══════════════════════════════════════════════════════════════════
Procesar comando JSON del controlador
Process JSON command from the controller
═══════════════════════════════════════════════════════════════════
_type == "noop" → no hacer nada
_type == "noop" → do nothing
_type == "cmd" → set-adopt / reboot / reset / locate
_type == "setstate" → aplicar radio_table + vap_table via UCI
_type == "setparam" → cambiar un parámetro individual
_type == "setstate" → apply radio_table + vap_table via UCI
_type == "setparam" → change a single parameter
*/
static void handle_response(openuf_state_t *st,
const uf_model_t *model,
@@ -896,7 +896,7 @@ static void handle_response(openuf_state_t *st,
system("reboot &");
} else if (!strcmp(cmd, "locate")) {
/* Parpadear LED — en OpenWrt: echo 1 > /sys/class/leds/.../trigger */
/* Blink LED — on OpenWrt: echo 1 > /sys/class/leds/.../trigger */
strcpy(action_out, "locate");
} else {
snprintf(action_out, 64, "cmd:%s", cmd);
@@ -904,7 +904,7 @@ static void handle_response(openuf_state_t *st,
return;
}
/* ── setstate — configuración WiFi del controlador ──────────── */
/* ── setstate — WiFi configuration from the controller ──────────── */
if (!strcmp(type, "setstate")) {
if (json_object_object_get_ex(resp, "cfgversion", &v))
snprintf(st->cfgversion, sizeof(st->cfgversion),
@@ -937,7 +937,7 @@ static void handle_response(openuf_state_t *st,
}
/* ═══════════════════════════════════════════════════════════════════
inform_send — función principal pública
inform_send — main public function
═══════════════════════════════════════════════════════════════════ */
int inform_send(openuf_state_t *st,
const uf_model_t *model,
@@ -961,7 +961,7 @@ int inform_send(openuf_state_t *st,
LOG("Sending inform: adopted=%d, authkey=%.8s..., inform_url=%s",
st->adopted, key_hex, st->inform_url);
/* MAC sin colones */
/* MAC without colons */
char mac_hex[32] = {0};
{
const char *s = st->mac; int j = 0;
+28 -29
View File
@@ -1,33 +1,33 @@
/*
* openuf - lldp.c
*
* LLDP completo: envío de frames propios + lectura de vecinos.
* Complete LLDP: sending of own frames + reading of neighbors.
*
* ── Construcción del frame ────────────────────────────────────────
* ── Frame construction ─────────────────────────────────────────────
*
* Los TLVs LLDP tienen cabecera de 2 bytes:
* bit 15..9 → tipo (7 bits)
* bit 8..0 → longitud (9 bits, max 511 bytes)
* LLDP TLVs have a 2-byte header:
* bit 15..9 → type (7 bits)
* bit 8..0 → length (9 bits, max 511 bytes)
*
* uint16_t header_be = (type << 9) | (len & 0x1ff)
*
* Ejemplo: Chassis ID TLV (type=1), 7 bytes de valor:
* Example: Chassis ID TLV (type=1), 7 bytes of value:
* header = (1 << 9) | 7 = 0x0207
* → bytes: 0x02 0x07 [subtype=4] [MAC 6 bytes]
*
* ── Envío con AF_PACKET ───────────────────────────────────────────
* ── Sending with AF_PACKET ────────────────────────────────────────
*
* 1. socket(AF_PACKET, SOCK_RAW, htons(0x88cc))
* 2. ioctl(SIOCGIFINDEX) → ifindex
* 3. Construir frame completo en buffer
* 4. sendto() con sockaddr_ll
* 3. Build the complete frame in a buffer
* 4. sendto() with sockaddr_ll
*
* Sin CAP_NET_RAW (no root) → socket() devuelve EPERM.
* Lo ignoramos silenciosamente (LLDP es opcional).
* Without CAP_NET_RAW (not root) → socket() returns EPERM.
* This is silently ignored (LLDP is optional).
*
* ── Lectura de vecinos con lldpctl ───────────────────────────────
* ── Reading neighbors with lldpctl ───────────────────────────────
*
* lldpctl -f json retorna:
* lldpctl -f json returns:
* {
* "lldp": {
* "interface": [
@@ -66,12 +66,12 @@
#include "lldp.h"
/* ─── Constantes ────────────────────────────────────────────────── */
/* ─── Constants ────────────────────────────────────────────────── */
static const uint8_t LLDP_DST[6] = {0x01,0x80,0xc2,0x00,0x00,0x0e};
#define LLDP_ETHERTYPE 0x88cc
#define CAP_WLAN_AP 0x0040
/* ─── Escribir TLV en buffer ────────────────────────────────────── */
/* ─── Write TLV to buffer ────────────────────────────────────── */
static int tlv_write(uint8_t *buf, int pos, int maxlen,
int type, const uint8_t *val, int vlen)
{
@@ -90,7 +90,7 @@ static int tlv_str(uint8_t *buf, int pos, int maxlen,
(const uint8_t*)str, (int)strlen(str));
}
/* ─── Parsear MAC "aa:bb:cc:dd:ee:ff" → bytes ──────────────────── */
/* ─── Parse MAC "aa:bb:cc:dd:ee:ff" → bytes ──────────────────── */
static void parse_mac(const char *s, uint8_t out[6])
{
unsigned int b[6]={0};
@@ -109,7 +109,7 @@ int lldp_send_frame(const char *ifname,
{
/* Socket raw — requiere root */
int fd = socket(AF_PACKET, SOCK_RAW, htons(LLDP_ETHERTYPE));
if (fd < 0) return -1; /* EPERM sin root → silencioso */
if (fd < 0) return -1; /* EPERM without root → silent */
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
@@ -162,9 +162,9 @@ int lldp_send_frame(const char *ifname,
0x00, (uint8_t)(CAP_WLAN_AP >> 8),
0x00, (uint8_t)(CAP_WLAN_AP & 0xff)
};
/* Corregir: CAP_WLAN_AP = 0x0040, un solo byte basta */
/* Fix: CAP_WLAN_AP = 0x0040, a single byte is enough */
v[1] = 0x00; v[0] = 0x00;
/* bit 6 de los 16 bits de capabilities */
/* bit 6 of the 16 capability bits */
uint16_t cap = CAP_WLAN_AP;
v[0] = (cap >> 8) & 0xff; v[1] = cap & 0xff;
v[2] = v[0]; v[3] = v[1]; /* enabled = same */
@@ -197,16 +197,15 @@ bool lldp_available(void)
}
/* ═══════════════════════════════════════════════════════════════════
lldp_read_neighbors — parsea JSON de lldpctl
lldp_read_neighbors — parses JSON from lldpctl
═══════════════════════════════════════════════════════════════════
Navega: root → "lldp" → "interface" (array) → cada vecino.
Por cada vecino extrae: chassis.id, chassis.name, chassis.descr,
port.id, port.descr, y el nombre de la interfaz local.
El resultado se incluye en lldp_table[] del payload inform.
El controlador lo usa para dibujar las líneas de conexión en
la topología visual (qué switch/puerto conecta a este AP).
Navigate: root → "lldp" → "interface" (array) → each neighbor.
For each neighbor, extract: chassis.id, chassis.name, chassis.descr,
port.id, port.descr, and the name of the local interface.
The result is included in lldp_table[] of the inform payload.
The controller uses it to draw the connection lines in
the visual topology (which switch/port this AP connects to).
*/
struct json_object *lldp_read_neighbors(void)
{
@@ -230,7 +229,7 @@ struct json_object *lldp_read_neighbors(void)
struct json_object *root = json_tokener_parse(buf);
if (!root) return result;
/* Navegar: root.lldp.interface[] */
/* Browse: root.lldp.interface[] */
struct json_object *lldp_o, *iface_arr;
if (!json_object_object_get_ex(root, "lldp", &lldp_o)) goto done;
if (!json_object_object_get_ex(lldp_o, "interface", &iface_arr)) goto done;
@@ -241,7 +240,7 @@ struct json_object *lldp_read_neighbors(void)
struct json_object *iface = json_object_array_get_idx(iface_arr, i);
if (!iface) continue;
/* Puerto local */
/* Local port */
struct json_object *tmp_o;
const char *local_port = "";
if (json_object_object_get_ex(iface, "name", &tmp_o))
+20 -20
View File
@@ -6,18 +6,18 @@
*
* LLDP (Link Layer Discovery Protocol — IEEE 802.1AB)
*
* ── ENVÍO de frames LLDP propios ────────────────────────────────
* ── SENDING our own LLDP frames ────────────────────────────────
*
* El AP transmite frames LLDP por cada puerto ethernet.
* Esto permite al switch upstream registrar al AP como vecino,
* y al controlador UniFi construir el mapa de topología visual.
* The AP transmits LLDP frames on each Ethernet port.
* This allows the upstream switch to register the AP as a neighbor,
* and lets the UniFi controller build the visual topology map.
*
* Frame Ethernet:
* dst = 01:80:c2:00:00:0e (multicast LLDP estándar)
* dst = 01:80:c2:00:00:0e (standard multicast LLDP)
* src = MAC del AP
* type = 0x88cc
*
* Payload (TLVs encadenados):
* Payload (TLVs chained):
* Header TLV = [type:7bits | len_hi:1bit][len_lo:8bits]
*
* TLV type=1 Chassis ID subtype=4(MAC), value=MAC[6]
@@ -28,12 +28,12 @@
* TLV type=7 Capabilities cap=0x0040(WLAN-AP), en=0x0040
* TLV type=0 End of LLDPDU len=0
*
* ── LECTURA de vecinos: lldpctl -f json ─────────────────────────
* ── READING of neighbors: lldpctl -f json ─────────────────────────
*
* Si lldpd está instalado, leemos los vecinos detectados
* y los incluimos en lldp_table del payload inform.
* If lldpd is installed, the detected neighbors are read
* and included in lldp_table of the inform payload.
*
* lldp_table en el JSON inform:
* lldp_table in the inform JSON:
* [{
* "local_port": "eth0",
* "chassis_id": "aa:bb:cc:...",
@@ -43,30 +43,30 @@
* "port_desc": "to-AP"
* }]
*
* ── SIN lldpd ───────────────────────────────────────────────────
* ── Without lldpd ───────────────────────────────────────────────────
*
* lldp_send_frame() funciona sin lldpd (usa raw socket directo).
* lldp_read_neighbors() retorna array vacío si no hay lldpctl.
* lldp_send_frame() works without lldpd (it uses a direct raw socket).
* lldp_read_neighbors() returns an empty array if lldpctl is not present.
*/
#include <stdbool.h>
#include <json-c/json.h>
/* Envía un frame LLDP por raw socket AF_PACKET.
* Requiere ejecutar como root (CAP_NET_RAW).
* Devuelve 0 si ok, -1 si error (sin root → error silencioso). */
/* Sends an LLDP frame via raw AF_PACKET socket.
* Requires running as root (CAP_NET_RAW).
* Returns 0 on success, -1 on error (without root → silent error). */
int lldp_send_frame(const char *ifname,
const char *mac_str,
const char *hostname,
const char *model_desc,
int ttl);
/* Lee vecinos LLDP de lldpctl y retorna JSON array lldp_table.
* Si lldpctl no está, retorna array vacío (no falla).
* Caller libera con json_object_put(). */
/* Reads LLDP neighbors from lldpctl and returns a JSON array lldp_table.
* If lldpctl is not present, returns an empty array (does not fail).
* Caller frees it with json_object_put(). */
struct json_object *lldp_read_neighbors(void);
/* true si lldpctl está instalado */
/* true if lldpctl is installed */
bool lldp_available(void);
#endif /* OPENUF_LLDP_H */
+9 -9
View File
@@ -1,10 +1,10 @@
/*
* openuf - main.c
*
* Daemon principal. Bucle con tres tareas:
* 1. Announce UDP broadcast+multicast cada 10s (descubrimiento L2)
* 2. Inform HTTP POST cifrado cada 10s (adopción + telemetría)
* 3. LLDP Raw frame L2 cada 30s (topología visual en UniFi)
* Main daemon. Loop with three tasks:
* 1. Announce UDP broadcast+multicast each 10s (discovery L2)
* 2. Inform HTTP POST cifrado each 10s (adoption + telemetrics)
* 3. LLDP Raw frame L2 each 30s (visual topology in UniFi)
*/
#include <stdio.h>
@@ -127,13 +127,13 @@ int main(int argc, char *argv[])
}
}
/* ── Descripción LLDP del dispositivo ───────────────────────── */
/* ── LLDP device description ───────────────────────── */
char lldp_desc[128];
snprintf(lldp_desc, sizeof(lldp_desc),
"%s %s%s (openuf)",
model->model_display, model->fw_pre, model->fw_ver);
/* ── Bucle principal ─────────────────────────────────────────── */
/* ── Main loop ─────────────────────────────────────────── */
time_t start_time = time(NULL);
time_t last_announce = 0;
time_t last_inform = 0;
@@ -154,12 +154,12 @@ int main(int argc, char *argv[])
last_announce = now;
}
/* LLDP frames por cada interfaz ethernet */
/* LLDP frames for each Ethernet interface */
if ((now - last_lldp) >= LLDP_INTERVAL) {
LOG("Sending LLDP frames");
for (int i = 0; i < model->port_table_len; i++) {
const char *iface = model->port_table[i].ifname;
/* Leer MAC real de la interfaz si disponible */
/* Read the actual MAC of the interface if available */
char iface_mac[32];
if (get_mac(iface, iface_mac, sizeof(iface_mac)) != 0)
strncpy(iface_mac, mac_str, sizeof(iface_mac)-1);
@@ -178,7 +178,7 @@ int main(int argc, char *argv[])
last_inform = now;
LOG("Sending inform");
/* Actualizar IP en cada ciclo */
/* Update IP each cycle */
char new_ip[64] = {0};
if (get_ip(cfg.lan_if, new_ip, sizeof(new_ip)) == 0 ||
get_ip("eth0", new_ip, sizeof(new_ip)) == 0)
+1 -1
View File
@@ -166,7 +166,7 @@ const uf_model_t model_uapg2aclr = {
.radio_map=uapg2aclr_rmap, .radio_map_len=2,
};
/* ─── Registro de modelos ─────────────────────────────────────── */
/* ─── Model Registry ─────────────────────────────────────── */
static const uf_model_t *all_models[] = {
&model_u6inwall,
&model_u6lite,
+23 -23
View File
@@ -1,26 +1,26 @@
/*
* openuf - sysinfo.c
*
* Lee estasticas del sistema para el payload inform.
* Reads system statistics for the inform payload.
*
* ── CPU: /proc/stat ──────────────────────────────────────────────────
*
* Formato: cpu user nice system idle iowait irq softirq steal
* Format: cpu user nice system idle iowait irq softirq steal
*
* El uso se calcula con dos snapshots separados en el tiempo:
* activo = user + nice + system + irq + softirq + steal
* total = activo + idle + iowait
* uso % = (Δactivo / Δtotal) × 100
* Usage is calculated with two snapshots taken at different times:
* active = user + nice + system + irq + softirq + steal
* total = active + idle + iowait
* usage % = (Δactive / Δtotal) × 100
*
* ── Memoria: /proc/meminfo ───────────────────────────────────────────
* ── Memory: /proc/meminfo ───────────────────────────────────────────
*
* MemTotal, MemFree, Buffers, Cached
* used = total - free - buffers - cached
*
* ── Interfaces: /proc/net/dev + /sys/class/net/<iface>/ ─────────────
*
* /proc/net/dev → contadores acumulados rx/tx
* /sys/class/net/speed → velocidad negociada (Mbps)
* /proc/net/dev → cumulative rx/tx counters
* /sys/class/net/speed → negotiated speed (Mbps)
* /sys/class/net/duplex → "full" / "half"
* /sys/class/net/operstate → "up" / "down" / "unknown"
* /sys/class/net/address → MAC
@@ -28,8 +28,8 @@
*
* ── Radio: iw dev <iface> info + survey dump ─────────────────────────
*
* info: canal actual, potencia TX
* survey dump: active/busy/tx/rx time → calcular % utilización
* info: current channel, TX power
* survey dump: active/busy/tx/rx time → calculate % utilization
*/
#define _GNU_SOURCE
@@ -47,7 +47,7 @@
#include "sysinfo.h"
/* ═══════════════════════════════════════════════════════════════════
Memoria
Memory
═══════════════════════════════════════════════════════════════════ */
int sysinfo_mem(mem_stats_t *out)
{
@@ -109,7 +109,7 @@ int sysinfo_cpu_percent(void)
}
/* ═══════════════════════════════════════════════════════════════════
Interfaz de red
Network interface
═══════════════════════════════════════════════════════════════════ */
static int read_sysfs_str(const char *iface, const char *file,
char *out, size_t sz)
@@ -167,7 +167,7 @@ int sysinfo_iface(const char *ifname, iface_stats_t *out)
/* IP */
read_ip_ioctl(ifname, out->ip, sizeof(out->ip));
/* Contadores de /proc/net/dev */
/* Counters of /proc/net/dev */
FILE *f = fopen("/proc/net/dev", "r");
if (!f) return 0;
@@ -179,7 +179,7 @@ int sysinfo_iface(const char *ifname, iface_stats_t *out)
char *colon = strchr(line, ':');
if (!colon) continue;
/* Extraer nombre de interfaz (puede tener espacios al inicio) */
/* Extract interface name (may have leading spaces) */
size_t end = colon - line;
while (end > 0 && line[end-1] == ' ') end--;
size_t start = 0;
@@ -210,24 +210,24 @@ int sysinfo_iface(const char *ifname, iface_stats_t *out)
}
/* ═══════════════════════════════════════════════════════════════════
Radio WiFi
WiFi Radio
═══════════════════════════════════════════════════════════════════
1. iw dev wlan0 info → canal y potencia
Ejemplo:
1. iw dev wlan0 info → channel and power
Example:
Interface wlan0
channel 6 (2437 MHz), width: 20 MHz
txpower 20.00 dBm
2. iw dev wlan0 survey dump → utilización del canal
Buscamos el bloque con "[in use]":
2. iw dev wlan0 survey dump → channel utilization
We look for the block with "[in use]":
frequency: 2437 MHz [in use]
channel active time: 12345 ms
channel busy time: 987 ms
channel transmit time: 456 ms
channel receive time: 321 ms
Calculamos:
We calculate:
cu_total = busy/active × 100
cu_self_tx = transmit/active × 100
cu_self_rx = receive/active × 100
@@ -268,7 +268,7 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
in_use = 1; active=busy=tx_t=rx_t=0; continue;
}
if (!in_use) continue;
/* Nueva frecuencia sin [in use] resetea el bloque */
/* New frequency without [in use] resets the block */
if (strstr(line, "frequency:") && !strstr(line, "[in use]")) {
in_use = 0; continue;
}
@@ -287,7 +287,7 @@ int sysinfo_radio(const char *iface, radio_stats_t *out)
out->cu_self_rx = (int)(rx_t * 100 / active);
}
/* Número de clientes asociados */
/* Number of associated clients */
snprintf(cmd, sizeof(cmd),
"iw dev %s station dump 2>/dev/null | grep -c '^Station'",
iface);
+14 -14
View File
@@ -4,15 +4,15 @@
/*
* openuf - sysinfo.h
*
* Lee estasticas del sistema (CPU, RAM, interfaces, radios).
* Todas las lecturas son del kernel Linux directamente:
* Reads system statistics (CPU, RAM, interfaces, radios).
* All readings come directly from the Linux kernel:
*
* /proc/stat → uso CPU (deltas entre dos snapshots)
* /proc/meminfo → memoria total/libre/buffer/cache
* /proc/net/dev → contadores rx/tx por interfaz
* /proc/stat → CPU usage (deltas between two snapshots)
* /proc/meminfo → total/free/buffer/cache memory
* /proc/net/dev → rx/tx counters per interface
* /sys/class/net/ → speed, duplex, operstate, MAC
* iw dev <if> info → canal actual, potencia TX
* iw dev <if> survey dump → utilización del canal
* iw dev <if> info → current channel, TX power
* iw dev <if> survey dump → channel utilization
*/
#include <stdbool.h>
@@ -28,9 +28,9 @@ typedef struct {
int sysinfo_mem(mem_stats_t *out);
/* ── CPU ─────────────────────────────────────────────────────────── */
/* Retorna % uso CPU (0-100). Primera llamada retorna 0 (toma snapshot).
* Las siguientes calculan el delta respecto a la anterior.
* Con intervalo de 10s da un buen promedio de uso. */
/* Returns % CPU usage (0-100). The first call returns 0 (takes a snapshot).
* Subsequent calls compute the delta relative to the previous one.
* With a 10s interval this gives a good average of usage. */
int sysinfo_cpu_percent(void);
/* ── Interfaz de red ─────────────────────────────────────────────── */
@@ -39,7 +39,7 @@ typedef struct {
char mac[32];
char ip[64];
bool up;
int speed; /* Mbps: 10/100/1000; -1 si no disponible */
int speed; /* Mbps: 10/100/1000; -1 if not available */
bool full_duplex;
long long rx_bytes;
long long tx_bytes;
@@ -60,9 +60,9 @@ typedef struct {
char iface[32];
int channel;
int tx_power;
int cu_total; /* % uso canal total */
int cu_self_tx; /* % tiempo transmitiendo */
int cu_self_rx; /* % tiempo recibiendo */
int cu_total; /* % total channel usage */
int cu_self_tx; /* % time spent transmitting */
int cu_self_rx; /* % time spent receiving */
int num_sta;
int noise; /* dBm */
} radio_stats_t;
+57 -56
View File
@@ -373,7 +373,7 @@ void wlan_clear(void)
return;
}
/* Recopilar secciones a eliminar (no modificar durante iteración) */
/* Collect sections to remove (do not modify during iteration) */
char *to_del[64];
int ndel = 0;
struct uci_element *e;
@@ -403,14 +403,14 @@ void wlan_clear(void)
}
/* ═══════════════════════════════════════════════════════════════════
wlan_apply_radio — aplicar config de radio (canal, HT, potencia)
wlan_apply_radio — apply radio config (channel, HT, power)
═══════════════════════════════════════════════════════════════════
Lectura de parámetros del JSON del controlador:
Reading parameters from the controller's JSON:
channel → wireless.<device>.channel
ht → wireless.<device>.htmode ("HT20" / "HT40" / "HT80" / "HE80")
tx_power → wireless.<device>.txpower
min_rssi → no se mapea a UCI (requiere daemon externo)
min_rssi → not mapped to UCI (requires an external daemon)
*/
void wlan_apply_radio(struct json_object *radio_json,
const char *device_name)
@@ -452,7 +452,7 @@ void wlan_apply_radio(struct json_object *radio_json,
RP("ht", "htmode");
/* Canal: 0 = auto en UniFi */
/* Channel: 0 = auto in UniFi */
if (json_object_object_get_ex(radio_json, "channel", &v)) {
int ch = json_object_get_int(v);
if (ch == 0) {
@@ -474,7 +474,7 @@ void wlan_apply_radio(struct json_object *radio_json,
uci_set(ctx, &ptr);
}
/* Habilitar el radio */
/* Enable the radio */
snprintf(path, sizeof(path), "wireless.%s.disabled=0", device_name);
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
@@ -488,22 +488,22 @@ void wlan_apply_radio(struct json_object *radio_json,
}
/* ═══════════════════════════════════════════════════════════════════
Crear una VAP (wifi-iface UCI) desde un JSON VAP del controlador
Create a VAP (wifi-iface UCI) from a controller VAP JSON
═══════════════════════════════════════════════════════════════════
Parámetros del controlador que leemos y cómo los mapeamos:
Controller parameters we read and how we map them:
essid → wireless.openuf_X.ssid
x_passphrase → wireless.openuf_X.key
security → wireless.openuf_X.encryption (via sec_to_uci)
hide_ssid → wireless.openuf_X.hidden
guest_policy → wireless.openuf_X.isolate (aislamiento de clientes)
fast_roaming_enabled → ieee80211r, ft_over_ds, mobility_domain, ft_psk_generate_local
band_steering → ieee80211k, ieee80211v, rrm_neighbor_report, bss_transition
pmf_mode → ieee80211w (0/1/2)
wpa3_support → añadir "sae-mixed" si WPA2+WPA3
uapsd → uapsd (U-APSD power saving)
vlan_id → wireless.openuf_X.vlan_id (si ≠ 0)
x_passphrase → wireless.openuf_X.key
security → wireless.openuf_X.encryption (via sec_to_uci)
hide_ssid → wireless.openuf_X.hidden
guest_policy → wireless.openuf_X.isolate (client isolation)
fast_roaming_enabled → ieee80211r, ft_over_ds, mobility_domain, ft_psk_generate_local
band_steering → ieee80211k, ieee80211v, rrm_neighbor_report, bss_transition
pmf_mode → ieee80211w (0/1/2)
wpa3_support → add "sae-mixed" if WPA2+WPA3
uapsd → uapsd (U-APSD power saving)
vlan_id → wireless.openuf_X.vlan_id (if ≠ 0)
*/
static int apply_vap(struct uci_context *ctx,
struct uci_package *pkg,
@@ -535,7 +535,7 @@ static int apply_vap(struct uci_context *ctx,
snprintf(target_network, sizeof(target_network), "vlan%d", vid);
}
/* Nombre de sección: openuf_<idx>_<ssid_safe> */
/* Section name: openuf_<idx>_<ssid_safe> */
char safe[16] = {0};
safe_section_name(essid, safe, sizeof(safe));
char sec_name[48];
@@ -556,7 +556,7 @@ static int apply_vap(struct uci_context *ctx,
* Preserve the controller's WLAN configuration ID. Inform telemetry must
* refer to this ObjectId; a label such as "user" is not a valid VAP ID.
* Controller versions use different keys, so accept the known variants.
*/
*/
const char *vap_id = NULL;
const char *id_keys[] = { "_id", "id", "wlanconf_id" };
for (size_t i = 0; i < sizeof(id_keys) / sizeof(id_keys[0]); i++) {
@@ -571,23 +571,23 @@ static int apply_vap(struct uci_context *ctx,
if (vap_id)
UCI_SET(ctx, "wireless", sec_name, "openuf_vap_id", vap_id);
/* Contraseña */
/* Password */
if (pass && pass[0] && strcmp(security,"open") != 0)
UCI_SET(ctx, "wireless", sec_name, "key", pass);
/* SSID oculto */
/* hidden SSID */
int hidden = 0;
if (json_object_object_get_ex(vap_json, "hide_ssid", &v))
hidden = json_object_get_boolean(v) ? 1 : 0;
UCI_SET_INT(ctx, "wireless", sec_name, "hidden", hidden);
/* Aislamiento de clientes (guest network) */
/* Client isolation (guest network) */
int isolate = 0;
if (json_object_object_get_ex(vap_json, "guest_policy", &v))
isolate = json_object_get_boolean(v) ? 1 : 0;
UCI_SET_INT(ctx, "wireless", sec_name, "isolate", isolate);
/* U-APSD (ahorro de energía para clientes móviles) */
/* U-APSD (power saving for mobile clients) */
int uapsd = 1;
if (json_object_object_get_ex(vap_json, "uapsd", &v))
uapsd = json_object_get_boolean(v) ? 1 : 0;
@@ -595,22 +595,22 @@ static int apply_vap(struct uci_context *ctx,
/* ── PMF (Protected Management Frames / 802.11w) ──────────────
* "disabled" → 0, "optional" → 1, "required" → 2
* WPA3 (sae/sae-mixed) siempre requiere ieee80211w=2 */
* WPA3 (sae/sae-mixed) always requires "optional" or " required" ieee80211w=2 */
int pmf = 0;
if (json_object_object_get_ex(vap_json, "pmf_mode", &v)) {
const char *pm = json_object_get_string(v);
if (!strcmp(pm, "optional")) pmf = 1;
if (!strcmp(pm, "required")) pmf = 2;
}
/* WPA3 obliga PMF=2 */
/* WPA3 forces PMF=2 */
if (!strcmp(security,"wpa3") || !strcmp(security,"wpa3transition") ||
!strcmp(security,"wpa3enterprise"))
pmf = 2;
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211w", pmf);
/* ── Fast Roaming (802.11r FT) ────────────────────────────────
* Permite que los clientes se muevan entre APs sin re-autenticación
* completa. El handshake FT sólo tarda ~50ms vs ~200-300ms normal. */
* Allows clients to move between APs without re-authentication
* complete. The FT handshake only takes ~50ms vs ~200-300ms for a normal one. */
const char *ft_keys[] = {
"fast_roaming_enabled", "fast_roaming", "ft_enabled", "ieee80211r"
};
@@ -629,10 +629,10 @@ static int apply_vap(struct uci_context *ctx,
}
/* ── Band Steering (802.11k/v) ────────────────────────────────
* 802.11k: Neighbor Reports → el AP informa al cliente qué otros
* APs existen para facilitar el roaming.
* 802.11v: BSS Transition Management → el AP puede "sugerir" al
* cliente que se mueva a otro AP con mejor señal. */
* 802.11k: Neighbor Reports → the AP tells the client what other
* APs exist to facilitate roaming.
* 802.11v: BSS Transition Management → the AP can "suggest" to the
* client to move to another AP with better signal.*/
int band_steer = 0;
if (json_object_object_get_ex(vap_json, "band_steering", &v))
band_steer = json_object_get_boolean(v) ? 1 : 0;
@@ -669,18 +669,18 @@ static int apply_vap(struct uci_context *ctx,
}
/* ═══════════════════════════════════════════════════════════════════
wlan_apply_config — aplicar configuración completa del controlador
wlan_apply_config — apply the controller's full configuration
═══════════════════════════════════════════════════════════════════
Llamado desde inform.c → handle_response() cuando _type=="setstate".
config_json es el JSON completo del controlador.
Called from inform.c → handle_response() when _type=="setstate".
config_json is the controller's complete JSON.
Proceso:
1. Eliminar VAPs antiguas (prefijo openuf_)
2. Aplicar radio_table (canal, potencia, htmode) por radio
3. Crear una VAP por cada entrada en vap_table
4. Hacer commit UCI
5. Ejecutar "wifi reload" para aplicar sin reiniciar el AP
Process:
1. Remove old VAPs (openuf_ prefix)
2. Apply radio_table (channel, power, htmode) per radio
3. Create one VAP for each entry in vap_table
4. Commit UCI
5. Run "wifi reload" to apply without rebooting the AP
*/
int wlan_apply_config(struct json_object *config_json,
const uf_model_t *model)
@@ -689,7 +689,7 @@ int wlan_apply_config(struct json_object *config_json,
json_object_object_get_ex(config_json, "radio_table", &rt_arr);
json_object_object_get_ex(config_json, "vap_table", &vt_arr);
/* Obtener MAC del AP para mobility_domain */
/* Get the AP's MAC for mobility_domain */
char mac_str[32] = "00:00:00:00:00:00";
{
char path[128];
@@ -706,13 +706,13 @@ int wlan_apply_config(struct json_object *config_json,
/* Remove every existing VAP so UniFi becomes the sole Wi-Fi owner. */
wlan_clear();
/* 2. Aplicar radio_table */
/* 2. Apply radio_table */
if (rt_arr && json_object_is_type(rt_arr, json_type_array)) {
int nr = json_object_array_length(rt_arr);
for (int i = 0; i < nr; i++) {
struct json_object *r = json_object_array_get_idx(rt_arr, i);
if (!r) continue;
/* Buscar el device UCI correspondiente a esta banda */
/* Find the UCI device corresponding to this band */
const char *radio_band = "";
if (json_object_object_get_ex(r, "radio", &v))
radio_band = json_object_get_string(v);
@@ -766,7 +766,7 @@ int wlan_apply_config(struct json_object *config_json,
printf("[openuf] Disabled %d default OpenWrt VAPs\n",
disabled_defaults);
/* 3. Crear VAPs */
/* 3. Create VAPs */
if (vt_arr && json_object_is_type(vt_arr, json_type_array)) {
int nv = json_object_array_length(vt_arr);
for (int i = 0; i < nv; i++) {
@@ -974,24 +974,25 @@ int wlan_apply_system_cfg(const char *system_cfg,
}
/* ═══════════════════════════════════════════════════════════════════
wlan_get_vap_table — leer VAPs activas desde UCI
wlan_get_vap_table — read active VAPs from UCI
═══════════════════════════════════════════════════════════════════
Itera todas las wifi-iface con prefijo "openuf_" en /etc/config/wireless
y construye el JSON vap_table para incluir en el payload inform.
Iterates over all wifi-iface entries with the "openuf_" prefix in
/etc/config/wireless and builds the vap_table JSON to include in
the inform payload.
Campos que leemos de UCI → campos en el JSON:
Fields we read from UCI → fields in the JSON:
ssid → essid
device → (usado para buscar radio y BSSID)
device → (used to look up radio and BSSID)
encryption → security (via sec_to_unifi)
hidden → hide_ssid
ieee80211r → fast_roaming_enabled
ieee80211k → band_steering
ieee80211w → pmf_mode ("disabled"/"optional"/"required")
disabled → up (inverso)
disabled → up (inverse)
También intentamos leer el BSSID real de la interfaz wlan
desde /sys/class/net/<iface>/address.
We also try to read the actual BSSID of the wlan interface
from /sys/class/net/<iface>/address.
*/
/* Resolve a configured VAP to the live interface reported by nl80211. */
static int find_runtime_vap(int phy_index, const char *ssid,
@@ -1044,7 +1045,7 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model)
uci_foreach_element(&pkg->sections, e) {
struct uci_section *sec = uci_to_section(e);
if (strcmp(sec->type, "wifi-iface") != 0) continue;
/* Solo reportar VAPs gestionadas por openuf */
/* Only report VAPs managed by openuf */
if (strncmp(sec->e.name, "openuf_", 7) != 0) continue;
#define UCI_GET(opt) uci_lookup_option_string(ctx, sec, opt)
@@ -1063,7 +1064,7 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model)
if (!ssid) ssid = "";
if (!device) device = "radio0";
/* Banda de este radio */
/* Band of this radio */
const char *radio_band = "ng";
for (int j = 0; j < model->radio_map_len; j++) {
if (!strcmp(model->radio_map[j].device, device)) {
@@ -1079,7 +1080,7 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model)
if (find_runtime_vap(ridx, ssid, wlan_iface, sizeof(wlan_iface)) != 0)
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", ridx);
/* Leer BSSID real desde sysfs */
/* Read the actual BSSID from sysfs */
char bssid[32] = "00:00:00:00:00:00";
{
char path[128];