Files
OpenUniFi/src/sysinfo.c
T
2026-07-14 23:09:21 +01:00

547 lines
20 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* openuf - sysinfo.c
*
* Reads system statistics for the inform payload.
*
* ── CPU: /proc/stat ──────────────────────────────────────────────────
*
* Format: cpu user nice system idle iowait irq softirq steal
*
* 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
*
* ── Memory: /proc/meminfo ────────────────────────────────────────────
*
* MemTotal, MemFree, Buffers, Cached
* used = total - free - buffers - cached
*
* ── Interfaces: /proc/net/dev + /sys/class/net/<iface>/ ─────────────
*
* /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
* ioctl SIOCGIFADDR → IP
*
* ── Radio: iw dev <iface> info + survey dump ─────────────────────────
*
* info: current channel, TX power
* survey dump: active/busy/tx/rx time → calculate % utilization
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <time.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "sysinfo.h"
/* ═══════════════════════════════════════════════════════════════════
Memory
═══════════════════════════════════════════════════════════════════ */
int sysinfo_mem(mem_stats_t *out)
{
memset(out, 0, sizeof(*out));
FILE *f = fopen("/proc/meminfo", "r");
if (!f) return -1;
char line[128];
while (fgets(line, sizeof(line), f)) {
long val = 0;
if (sscanf(line, "MemTotal: %ld kB", &val) == 1) out->total_kb = val;
else if (sscanf(line, "MemFree: %ld kB", &val) == 1) out->free_kb = val;
else if (sscanf(line, "Buffers: %ld kB", &val) == 1) out->buffer_kb = val;
else if (sscanf(line, "Cached: %ld kB", &val) == 1) out->cached_kb = val;
}
fclose(f);
return (out->total_kb > 0) ? 0 : -1;
}
/* ═══════════════════════════════════════════════════════════════════
CPU
═══════════════════════════════════════════════════════════════════ */
typedef struct {
unsigned long long user, nice, sys, idle, iowait, irq, softirq, steal;
} cpu_snap_t;
static cpu_snap_t g_prev = {0};
static int g_valid = 0;
static int read_cpu(cpu_snap_t *s)
{
FILE *f = fopen("/proc/stat", "r");
if (!f) return -1;
int r = fscanf(f, "cpu %llu %llu %llu %llu %llu %llu %llu %llu",
&s->user, &s->nice, &s->sys, &s->idle,
&s->iowait, &s->irq, &s->softirq, &s->steal);
fclose(f);
return (r >= 4) ? 0 : -1;
}
int sysinfo_cpu_percent(void)
{
cpu_snap_t cur;
if (read_cpu(&cur) != 0) return 0;
if (!g_valid) { g_prev = cur; g_valid = 1; return 0; }
unsigned long long da = (cur.user - g_prev.user)
+ (cur.nice - g_prev.nice)
+ (cur.sys - g_prev.sys)
+ (cur.irq - g_prev.irq)
+ (cur.softirq - g_prev.softirq)
+ (cur.steal - g_prev.steal);
unsigned long long di = (cur.idle - g_prev.idle)
+ (cur.iowait - g_prev.iowait);
unsigned long long dt = da + di;
g_prev = cur;
return (dt == 0) ? 0 : (int)((da * 100) / dt);
}
/* ═══════════════════════════════════════════════════════════════════
Network interface
═══════════════════════════════════════════════════════════════════ */
static int read_sysfs_str(const char *iface, const char *file,
char *out, size_t sz)
{
char path[128];
snprintf(path, sizeof(path), "/sys/class/net/%s/%s", iface, file);
FILE *f = fopen(path, "r");
if (!f) return -1;
char buf[64] = {0};
fgets(buf, sizeof(buf), f);
fclose(f);
buf[strcspn(buf, "\r\n")] = '\0';
strncpy(out, buf, sz - 1);
return (strlen(out) > 0) ? 0 : -1;
}
static int read_sysfs_int(const char *iface, const char *file)
{
char buf[32] = {0};
if (read_sysfs_str(iface, file, buf, sizeof(buf)) != 0) return -1;
int v = -1; sscanf(buf, "%d", &v); return v;
}
static void read_ip_ioctl(const char *iface, char *out, size_t sz)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) return;
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, iface, IFNAMSIZ - 1);
if (ioctl(fd, SIOCGIFADDR, &ifr) == 0) {
struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_addr;
strncpy(out, inet_ntoa(sa->sin_addr), sz - 1);
}
close(fd);
}
int sysinfo_iface(const char *ifname, iface_stats_t *out)
{
memset(out, 0, sizeof(*out));
strncpy(out->name, ifname, sizeof(out->name) - 1);
/* MAC, operstate, speed, duplex */
read_sysfs_str(ifname, "address", out->mac, sizeof(out->mac));
char opstate[32] = {0};
read_sysfs_str(ifname, "operstate", opstate, sizeof(opstate));
out->up = (strcmp(opstate, "up") == 0 || strcmp(opstate, "unknown") == 0);
int sp = read_sysfs_int(ifname, "speed");
out->speed = (sp > 0) ? sp : 1000;
char dup[16] = {0};
read_sysfs_str(ifname, "duplex", dup, sizeof(dup));
out->full_duplex = (strncmp(dup, "full", 4) == 0);
/* IP */
read_ip_ioctl(ifname, out->ip, sizeof(out->ip));
/* Counters of /proc/net/dev */
FILE *f = fopen("/proc/net/dev", "r");
if (!f) return 0;
char line[512];
fgets(line, sizeof(line), f); /* skip header lines */
fgets(line, sizeof(line), f);
while (fgets(line, sizeof(line), f)) {
char *colon = strchr(line, ':');
if (!colon) continue;
/* Extract interface name (may have leading spaces) */
size_t end = colon - line;
while (end > 0 && line[end-1] == ' ') end--;
size_t start = 0;
while (start < end && line[start] == ' ') start++;
char name[32] = {0};
size_t nlen = end - start;
if (nlen >= sizeof(name)) continue;
strncpy(name, line + start, nlen);
if (strcmp(name, ifname) != 0) continue;
long long rb,rp,re,rd,rf,rframe,rcomp,rmulti;
long long tb,tp,te,td,tf,tcol,tcomp,tcarr;
sscanf(colon+1,
"%lld %lld %lld %lld %lld %lld %lld %lld"
" %lld %lld %lld %lld %lld %lld %lld %lld",
&rb,&rp,&re,&rd,&rf,&rframe,&rcomp,&rmulti,
&tb,&tp,&te,&td,&tf,&tcol,&tcomp,&tcarr);
out->rx_bytes = rb; out->rx_packets = rp;
out->rx_errors = re; out->rx_dropped = rd;
out->rx_frame = rframe;
out->rx_multicast= rmulti;
out->tx_bytes = tb; out->tx_packets = tp;
out->tx_errors = te; out->tx_dropped = td;
break;
}
fclose(f);
return 0;
}
/* ═══════════════════════════════════════════════════════════════════
WiFi Radio
═══════════════════════════════════════════════════════════════════
Survey counters are cumulative. Keep a small per-interface snapshot so
each inform reports utilization during the latest interval rather than an
average since the radio was started.
*/
typedef struct {
char iface[32];
long long active;
long long busy;
long long tx;
long long rx;
long long sta_tx_duration;
long long sta_rx_duration;
struct timespec duration_time;
int duration_valid;
int valid;
} survey_snapshot_t;
#define MAX_SURVEY_SNAPSHOTS 8
static survey_snapshot_t survey_snapshots[MAX_SURVEY_SNAPSHOTS];
static int utilization_percent(long long part, long long total)
{
if (part <= 0 || total <= 0) return 0;
long long value = (part * 100 + total / 2) / total;
if (value < 0) return 0;
if (value > 100) return 100;
return (int)value;
}
static int count_antenna_chains(unsigned int mask)
{
int count = 0;
while (mask) {
count += mask & 1U;
mask >>= 1;
}
return count;
}
/*
1. iw dev wlan0 info → channel and power
Example:
Interface wlan0
channel 6 (2437 MHz), width: 20 MHz
txpower 20.00 dBm
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
We calculate:
cu_total = busy/active × 100
cu_self_tx = transmit/active × 100
cu_self_rx = receive/active × 100
*/
int sysinfo_radio(const char *iface, radio_stats_t *out)
{
memset(out, 0, sizeof(*out));
strncpy(out->iface, iface, sizeof(out->iface) - 1);
out->noise = -95;
char cmd[128];
int current_freq = 0;
int wiphy_index = -1;
/* iw dev <iface> info */
snprintf(cmd, sizeof(cmd), "iw dev %s info 2>/dev/null", iface);
FILE *p = popen(cmd, "r");
if (!p) return -1;
char line[256];
while (fgets(line, sizeof(line), p)) {
int ch; float mhz;
if (sscanf(line, " channel %d (%f MHz)", &ch, &mhz) == 2) {
out->channel = ch;
current_freq = (int)(mhz + 0.5f);
}
int index;
if (sscanf(line, " wiphy %d", &index) == 1)
wiphy_index = index;
float tp;
if (sscanf(line, " txpower %f dBm", &tp) == 1)
out->tx_power = (int)tp;
}
pclose(p);
/* Read the physical radio rather than trusting the emulated model. */
if (wiphy_index >= 0) {
snprintf(cmd, sizeof(cmd), "iw phy phy%d info 2>/dev/null", wiphy_index);
p = popen(cmd, "r");
if (p) {
unsigned int available_tx = 0, available_rx = 0;
unsigned int configured_tx = 0, configured_rx = 0;
int max_mcs_nss = 0;
while (fgets(line, sizeof(line), p)) {
unsigned int tx_mask, rx_mask;
if (sscanf(line, " Configured Antennas: TX 0x%x RX 0x%x",
&tx_mask, &rx_mask) == 2) {
configured_tx = tx_mask;
configured_rx = rx_mask;
} else if (sscanf(line, " Available Antennas: TX 0x%x RX 0x%x",
&tx_mask, &rx_mask) == 2) {
available_tx = tx_mask;
available_rx = rx_mask;
}
int streams, top_mcs;
if (sscanf(line, " %d streams: MCS 0-%d", &streams, &top_mcs) == 2 &&
streams > max_mcs_nss)
max_mcs_nss = streams;
if (sscanf(line,
" HT TX/RX MCS rate indexes supported: 0-%d",
&top_mcs) == 1) {
int ht_nss = top_mcs / 8 + 1;
if (ht_nss > max_mcs_nss) max_mcs_nss = ht_nss;
}
}
pclose(p);
out->tx_antennas = count_antenna_chains(
configured_tx ? configured_tx : available_tx);
out->rx_antennas = count_antenna_chains(
configured_rx ? configured_rx : available_rx);
if (out->tx_antennas && out->rx_antennas)
out->nss = out->tx_antennas < out->rx_antennas
? out->tx_antennas : out->rx_antennas;
else
out->nss = max_mcs_nss;
}
}
/* iw dev <iface> survey dump */
snprintf(cmd, sizeof(cmd), "iw dev %s survey dump 2>/dev/null", iface);
p = popen(cmd, "r");
if (!p) return 0;
long long active=0, busy=0, tx_t=0, rx_t=0;
int selected = 0;
while (fgets(line, sizeof(line), p)) {
int survey_freq;
if (sscanf(line, " frequency: %d MHz", &survey_freq) == 1) {
/* Some drivers omit the optional [in use] marker, especially on
* the secondary radio. Match the frequency reported by iw info. */
selected = strstr(line, "[in use]") != NULL ||
(current_freq > 0 && survey_freq == current_freq);
if (selected)
active=busy=tx_t=rx_t=0;
continue;
}
if (!selected) continue;
float noise; long long val;
if (sscanf(line, " noise: %f dBm", &noise) == 1) out->noise = (int)noise;
if (sscanf(line, " channel active time: %lld ms", &val) == 1) active = val;
if (sscanf(line, " channel busy time: %lld ms", &val) == 1) busy = val;
if (sscanf(line, " channel transmit time: %lld ms", &val) == 1) tx_t = val;
if (sscanf(line, " channel receive time: %lld ms", &val) == 1) rx_t = val;
}
pclose(p);
long long sample_active = active;
long long sample_busy = busy;
long long sample_tx = tx_t;
long long sample_rx = rx_t;
survey_snapshot_t *snapshot = NULL;
survey_snapshot_t *free_slot = NULL;
for (int i = 0; i < MAX_SURVEY_SNAPSHOTS; i++) {
if (survey_snapshots[i].valid &&
!strcmp(survey_snapshots[i].iface, iface)) {
snapshot = &survey_snapshots[i];
break;
}
if (!survey_snapshots[i].valid && !free_slot)
free_slot = &survey_snapshots[i];
}
if (!snapshot) snapshot = free_slot;
if (snapshot && snapshot->valid && active > snapshot->active &&
busy >= snapshot->busy && tx_t >= snapshot->tx && rx_t >= snapshot->rx) {
sample_active = active - snapshot->active;
sample_busy = busy - snapshot->busy;
sample_tx = tx_t - snapshot->tx;
sample_rx = rx_t - snapshot->rx;
}
if (snapshot && active > 0) {
snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
snapshot->active = active;
snapshot->busy = busy;
snapshot->tx = tx_t;
snapshot->rx = rx_t;
snapshot->valid = 1;
}
if (sample_active > 0) {
out->cu_total = utilization_percent(sample_busy, sample_active);
out->cu_self_tx = utilization_percent(sample_tx, sample_active);
out->cu_self_rx = utilization_percent(sample_rx, sample_active);
}
/* Associated clients and counters exposed by nl80211. */
snprintf(cmd, sizeof(cmd), "iw dev %s station dump 2>/dev/null", iface);
p = popen(cmd, "r");
if (p) {
while (fgets(line, sizeof(line), p)) {
long long val;
if (!strncmp(line, "Station ", 8)) {
out->num_sta++;
} else if (sscanf(line, " tx packets: %lld", &val) == 1) {
out->tx_packets += val;
} else if (sscanf(line, " tx retries: %lld", &val) == 1) {
out->tx_retries += val;
} else if (sscanf(line, " tx failed: %lld", &val) == 1) {
out->tx_failed += val;
} else if (sscanf(line, " tx duration: %lld us", &val) == 1) {
out->tx_duration += val;
} else if (sscanf(line, " rx duration: %lld us", &val) == 1) {
out->rx_duration += val;
}
}
pclose(p);
}
/* Some drivers expose no survey busy time on their secondary radio but
* do expose per-station airtime durations. Use those interval counters as
* a conservative "This AP" fallback; interference remains zero because
* station data cannot measure neighboring transmitters. */
struct timespec now;
if (snapshot && clock_gettime(CLOCK_MONOTONIC, &now) == 0) {
long long elapsed_us = 0;
if (snapshot->duration_valid) {
elapsed_us = (now.tv_sec - snapshot->duration_time.tv_sec) * 1000000LL +
(now.tv_nsec - snapshot->duration_time.tv_nsec) / 1000LL;
}
if (snapshot->duration_valid && elapsed_us >= 1000000LL &&
out->tx_duration >= snapshot->sta_tx_duration &&
out->rx_duration >= snapshot->sta_rx_duration &&
out->cu_total == 0) {
long long tx_delta = out->tx_duration - snapshot->sta_tx_duration;
long long rx_delta = out->rx_duration - snapshot->sta_rx_duration;
out->cu_self_tx = utilization_percent(tx_delta, elapsed_us);
out->cu_self_rx = utilization_percent(rx_delta, elapsed_us);
out->cu_total = out->cu_self_tx + out->cu_self_rx;
if (out->cu_total > 100) out->cu_total = 100;
}
/* sysinfo_radio() is called more than once while building an inform.
* Ignore sub-second calls so they do not replace the interval base. */
if (!snapshot->duration_valid || elapsed_us >= 1000000LL) {
if (!snapshot->valid) {
snprintf(snapshot->iface, sizeof(snapshot->iface), "%s", iface);
snapshot->valid = 1;
}
snapshot->sta_tx_duration = out->tx_duration;
snapshot->sta_rx_duration = out->rx_duration;
snapshot->duration_time = now;
snapshot->duration_valid = 1;
}
}
return 0;
}
static int frequency_to_channel(int frequency)
{
if (frequency == 2484) return 14;
if (frequency >= 2412 && frequency <= 2472)
return (frequency - 2407) / 5;
if (frequency >= 5000 && frequency <= 5895)
return (frequency - 5000) / 5;
if (frequency >= 5955 && frequency <= 7115)
return (frequency - 5950) / 5;
return 0;
}
int sysinfo_wifi_scan_cache(const char *iface, wifi_scan_t *out, int max_out)
{
if (!iface || !out || max_out <= 0) return 0;
char cmd[128];
snprintf(cmd, sizeof(cmd), "iw dev %s scan dump 2>/dev/null", iface);
FILE *p = popen(cmd, "r");
if (!p) return 0;
int count = 0;
wifi_scan_t *cur = NULL;
char line[512];
while (fgets(line, sizeof(line), p)) {
char bssid[32];
if (sscanf(line, "BSS %31[^ (]", bssid) == 1) {
if (count >= max_out) {
cur = NULL;
continue;
}
cur = &out[count++];
memset(cur, 0, sizeof(*cur));
snprintf(cur->bssid, sizeof(cur->bssid), "%s", bssid);
continue;
}
if (!cur) continue;
int value;
float signal;
char essid[64];
if (sscanf(line, " freq: %d", &value) == 1) {
cur->frequency = value;
cur->channel = frequency_to_channel(value);
} else if (sscanf(line, " signal: %f dBm", &signal) == 1) {
cur->signal = (int)signal;
value = (cur->signal + 100) * 2;
cur->rssi = value < 0 ? 0 : value > 100 ? 100 : value;
} else if (sscanf(line, " last seen: %d ms ago", &value) == 1) {
cur->age = value / 1000;
} else if (sscanf(line, " SSID: %63[^\n]", essid) == 1) {
snprintf(cur->essid, sizeof(cur->essid), "%s", essid);
} else if (strstr(line, "capability:") && strstr(line, "Privacy")) {
cur->secured = true;
} else if (strstr(line, "RSN:") || strstr(line, "WPA:")) {
cur->secured = true;
}
}
pclose(p);
return count;
}