uzbek-plus/b14/src/aurora-ui.c

379 lines
14 KiB
C

/*
* aurora-ui — B14 128x128 modem status screen (SDL2 Wayland client under Cage).
*
* Rendering: one 128x128 ARGB buffer drawn in software (8x16 cp437 bitmap font), uploaded to a streaming
* texture once per second or when data changes. Max ~2 FPS, no animations.
*
* Data (all optional; a missing source shows "--" / "ERR", never crashes):
* main thread, 1 s : /sys/class/net/wwan0/statistics/{rx,tx}_bytes (throughput; bam-dmux keeps them at 0 -> wlan0 counters),
* /proc/net/route (default),
* /sys/kernel/debug/pm8916_lbc/state (B10 supervisor: state + VADC vbat),
* /sys/class/power_supply/pm8916-lbc-chgr/online, thermal zone "pm8916-thermal", clock
* worker, 2 s : /run/aurora-modem/state, lifecycle.log (START OK / FAIL), ipcfg (IPv4)
* worker, 5 s : `timeout 3 mmcli -m any --output-keyvalue` (operator, RAT, signal, state) — only while ModemManager runs,
* `timeout 3 iw dev wlan0 info` + `station dump` (AP up, client count)
* No SOC: the board has no fuel gauge, so only VBAT is shown.
*/
#include <SDL2/SDL.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <dirent.h>
#include "font8x16.h"
#define W 128
#define H 128
/* palette: 4 main colours + dim grey */
#define C_BG 0xff000000u
#define C_TEXT 0xffe8e8e8u
#define C_OK 0xff40d040u
#define C_WARN 0xffffc020u
#define C_BAD 0xffff4040u
#define C_DIM 0xff606060u
static uint32_t fb[W * H];
static volatile sig_atomic_t quit;
/* ---------- drawing ---------- */
static void fill(int x, int y, int w, int h, uint32_t c)
{
for (int j = y; j < y + h; j++)
for (int i = x; i < x + w; i++)
if (i >= 0 && i < W && j >= 0 && j < H) fb[j * W + i] = c;
}
static void glyph(int x, int y, unsigned char ch, uint32_t c)
{
const unsigned char *g = &font8x16[ch * 16];
for (int r = 0; r < 16; r++)
for (int b = 0; b < 8; b++)
if (g[r] & (0x80 >> b)) fill(x + b, y + r, 1, 1, c);
}
static void text(int x, int y, uint32_t c, const char *s)
{
for (; *s && x < W; s++, x += 8) glyph(x, y, (unsigned char)*s, c);
}
/* right-aligned to column 16 */
static void textr(int y, uint32_t c, const char *s)
{
text(W - 8 * (int)strlen(s), y, c, s);
}
static void bars(int x, int y, int pct, uint32_t c)
{
for (int i = 0; i < 5; i++) {
int h = 3 + i * 3, on = pct >= 0 && pct > i * 20;
fill(x + i * 6, y + 14 - h, 4, h, on ? c : C_DIM);
}
}
/* ---------- small file helpers ---------- */
static int readfile(const char *p, char *buf, size_t n)
{
FILE *f = fopen(p, "r");
if (!f) return -1;
size_t k = fread(buf, 1, n - 1, f);
fclose(f);
buf[k] = 0;
return (int)k;
}
static long long readll(const char *p)
{
char b[64];
return readfile(p, b, sizeof b) > 0 ? atoll(b) : -1;
}
static int pid_alive(const char *pidfile)
{
char b[32], p[64];
if (readfile(pidfile, b, sizeof b) <= 0) return 0;
snprintf(p, sizeof p, "/proc/%d", atoi(b));
return access(p, F_OK) == 0;
}
/* ---------- shared state ---------- */
struct state {
/* worker */
char mstate[24]; /* controller state, e.g. BEARER_CONNECTED */
int mstart; /* 0 starting, 1 START OK, -1 FAIL */
char ip[20];
char oper[16], rat[8];
int sig; /* % or -1 */
int mm_ok; /* mmcli answered */
int ap; /* 1 AP, 0 down, -1 unknown */
int clients;
/* main */
double rxr, txr; /* B/s */
int wwan_ok, defroute;
char chg[12]; /* CHARGING/HOLD/FAULT/... */
int vbat_mv, usb, tempc;
};
static struct state st = { .sig = -1, .ap = -1, .clients = -1, .vbat_mv = -1, .tempc = -1000 };
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
static int dirty = 1;
static void kv(const char *line, const char *key, char *out, size_t n)
{
const char *p = strstr(line, key);
if (!p) return;
p = strchr(p, ':');
if (!p) return;
p++;
while (*p == ' ') p++;
size_t k = strcspn(p, "\n");
if (k >= n) k = n - 1;
memcpy(out, p, k);
out[k] = 0;
}
static void *worker(void *arg)
{
(void)arg;
int tick = 0;
while (!quit) {
struct state t;
pthread_mutex_lock(&mu); t = st; pthread_mutex_unlock(&mu);
char b[4096];
if (readfile("/run/aurora-modem/state", b, sizeof b) > 0) { b[strcspn(b, "\n")] = 0; snprintf(t.mstate, sizeof t.mstate, "%s", b); }
else snprintf(t.mstate, sizeof t.mstate, "--");
t.mstart = 0;
FILE *f = fopen("/run/aurora-modem/lifecycle.log", "r");
if (f) {
char l[512];
while (fgets(l, sizeof l, f)) {
if (strstr(l, "=== START OK")) t.mstart = 1;
else if (strstr(l, "FAIL in") || strstr(l, "rollback")) t.mstart = -1;
else if (strstr(l, "=== START (")) t.mstart = 0;
}
fclose(f);
}
t.ip[0] = 0;
if (readfile("/run/aurora-modem/ipcfg", b, sizeof b) > 0) {
char *a = strstr(b, "\nA=");
if (a) { a += 3; size_t k = strcspn(a, "\n"); if (k < sizeof t.ip) { memcpy(t.ip, a, k); t.ip[k] = 0; } }
}
if (tick % 5 == 0) {
/* LTE details from ModemManager, only if it runs (avoid spawning mmcli into a stopped stack) */
t.mm_ok = 0;
if (pid_alive("/run/aurora-modem/mm.pid") && (f = popen("timeout 3 mmcli -m any --output-keyvalue 2>/dev/null", "r"))) {
char l[512], v[64];
t.sig = -1; t.oper[0] = 0; t.rat[0] = 0;
while (fgets(l, sizeof l, f)) {
v[0] = 0;
if (strstr(l, "modem.3gpp.operator-name ")) { kv(l, "operator-name", v, sizeof v); snprintf(t.oper, sizeof t.oper, "%s", strcmp(v, "--") ? v : ""); t.mm_ok = 1; }
else if (strstr(l, "access-technologies.value[1]")) { kv(l, "]", v, sizeof v); snprintf(t.rat, sizeof t.rat, "%s", v); }
else if (strstr(l, "signal-quality.value")) { kv(l, "value", v, sizeof v); t.sig = atoi(v); }
}
pclose(f);
for (char *p = t.oper; *p; p++) if (*p >= 'a' && *p <= 'z') *p -= 32;
for (char *p = t.rat; *p; p++) if (*p >= 'a' && *p <= 'z') *p -= 32;
}
/* Wi-Fi AP + stations (one short iw call pair, every 5 s) */
t.ap = -1; t.clients = -1;
if ((f = popen("timeout 3 iw dev wlan0 info 2>/dev/null; echo ---; timeout 3 iw dev wlan0 station dump 2>/dev/null", "r"))) {
char l[256]; int sect = 0, n = 0, ap = 0, any = 0;
while (fgets(l, sizeof l, f)) {
if (!strncmp(l, "---", 3)) { sect = 1; continue; }
if (!sect) { any = 1; if (strstr(l, "type AP")) ap = 1; }
else if (!strncmp(l, "Station ", 8)) n++;
}
pclose(f);
if (any) { t.ap = ap; t.clients = ap ? n : 0; }
}
}
pthread_mutex_lock(&mu);
if (memcmp(&t, &st, sizeof t)) {
/* keep the main-thread fields current */
t.rxr = st.rxr; t.txr = st.txr; t.wwan_ok = st.wwan_ok; t.defroute = st.defroute;
memcpy(t.chg, st.chg, sizeof t.chg); t.vbat_mv = st.vbat_mv; t.usb = st.usb; t.tempc = st.tempc;
st = t; dirty = 1;
}
pthread_mutex_unlock(&mu);
for (int i = 0; i < 20 && !quit; i++) usleep(100000);
tick += 2;
}
return NULL;
}
/* ---------- fast metrics (main thread, 1 s) ---------- */
static long long prx = -1, ptx = -1;
static double pt;
static void fast(void)
{
char b[512];
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
double now = ts.tv_sec + ts.tv_nsec / 1e9;
long long rx = readll("/sys/class/net/wwan0/statistics/rx_bytes"), tx = readll("/sys/class/net/wwan0/statistics/tx_bytes");
/* the bam-dmux wwan0 driver keeps its netdev counters at 0 -> fall back to the Wi-Fi LAN side (traffic to/from clients):
download = wlan0 tx, upload = wlan0 rx */
if (rx == 0 && tx == 0) {
long long lrx = readll("/sys/class/net/wlan0/statistics/rx_bytes"), ltx = readll("/sys/class/net/wlan0/statistics/tx_bytes");
if (lrx >= 0 && ltx >= 0) { rx = ltx; tx = lrx; }
}
struct state t; pthread_mutex_lock(&mu); t = st; pthread_mutex_unlock(&mu);
t.wwan_ok = rx >= 0;
if (rx >= 0 && prx >= 0 && now > pt) { t.rxr = (rx - prx) / (now - pt); t.txr = (tx - ptx) / (now - pt); }
else { t.rxr = t.txr = -1; }
prx = rx; ptx = tx; pt = now;
t.defroute = 0;
FILE *f = fopen("/proc/net/route", "r");
if (f) { char l[256], ifn[32]; unsigned d; while (fgets(l, sizeof l, f)) if (sscanf(l, "%31s %x", ifn, &d) == 2 && d == 0) t.defroute = 1; fclose(f); }
t.chg[0] = 0; t.vbat_mv = -1;
if (readfile("/sys/kernel/debug/pm8916_lbc/state", b, sizeof b) > 0) {
char s[16]; long uv;
char *p = strstr(b, "state ");
if (p && sscanf(p, "state %15s", s) == 1) snprintf(t.chg, sizeof t.chg, "%s", s);
if ((p = strstr(b, "vbat_uv ")) && sscanf(p, "vbat_uv %ld", &uv) == 1) t.vbat_mv = (int)(uv / 1000);
}
t.usb = (int)readll("/sys/class/power_supply/pm8916-lbc-chgr/online");
t.tempc = -1000;
DIR *d = opendir("/sys/class/thermal");
if (d) {
struct dirent *e; char p[300];
while ((e = readdir(d))) {
if (strncmp(e->d_name, "thermal_zone", 12)) continue;
snprintf(p, sizeof p, "/sys/class/thermal/%s/type", e->d_name);
if (readfile(p, b, sizeof b) > 0 && !strncmp(b, "pm8916-thermal", 14)) {
snprintf(p, sizeof p, "/sys/class/thermal/%s/temp", e->d_name);
long long v = readll(p); if (v > -100000) t.tempc = (int)(v / 1000);
}
}
closedir(d);
}
pthread_mutex_lock(&mu);
st.rxr = t.rxr; st.txr = t.txr; st.wwan_ok = t.wwan_ok; st.defroute = t.defroute;
memcpy(st.chg, t.chg, sizeof st.chg); st.vbat_mv = t.vbat_mv; st.usb = t.usb; st.tempc = t.tempc;
pthread_mutex_unlock(&mu);
}
static void rate(char *o, size_t n, double r)
{
if (r < 0) snprintf(o, n, "--");
else if (r < 1000) snprintf(o, n, "%dB", (int)r);
else if (r < 1e6) snprintf(o, n, "%.*fK", r < 1e4 ? 1 : 0, r / 1e3);
else snprintf(o, n, "%.1fM", r / 1e6);
}
/* ---------- screen ---------- */
static void render(void)
{
struct state t; pthread_mutex_lock(&mu); t = st; pthread_mutex_unlock(&mu);
char s[40], a[16], b[16];
fill(0, 0, W, H, C_BG);
/* row 0-1: LTE */
int online = t.mstart == 1 && !strcmp(t.mstate, "BEARER_CONNECTED");
if (t.mstart == 0 && strcmp(t.mstate, "BEARER_CONNECTED")) {
text(0, 0, C_WARN, "MODEM STARTING");
snprintf(s, sizeof s, "%.16s", t.mstate); text(0, 16, C_DIM, s);
} else if (t.mstart < 0) {
text(0, 0, C_BAD, "MODEM FAIL");
snprintf(s, sizeof s, "%.16s", t.mstate); text(0, 16, C_DIM, s);
} else {
snprintf(s, sizeof s, "%.11s", t.oper[0] ? t.oper : "LTE --"); text(0, 0, t.mm_ok ? C_TEXT : C_DIM, s);
textr(0, online ? C_OK : C_WARN, t.rat[0] ? t.rat : "--");
bars(0, 16, t.mm_ok ? t.sig : -1, online ? C_OK : C_WARN);
if (t.mm_ok && t.sig >= 0) { snprintf(s, sizeof s, "%d%%", t.sig); text(36, 16, C_TEXT, s); }
else text(36, 16, C_DIM, "--");
}
/* row 2: IPv4 */
text(0, 32, t.ip[0] ? C_TEXT : C_DIM, t.ip[0] ? t.ip : "IP --");
/* row 3: Wi-Fi */
if (t.ap < 0) text(0, 48, C_DIM, "WiFi --");
else if (!t.ap) text(0, 48, C_BAD, "WiFi DOWN");
else { snprintf(s, sizeof s, "WiFi %d cl", t.clients); text(0, 48, C_OK, s); }
/* row 4: throughput (cp437 0x19 down, 0x18 up) */
rate(a, sizeof a, t.rxr); rate(b, sizeof b, t.txr);
snprintf(s, sizeof s, "\x19%-6s \x18%s", a, b); text(0, 64, t.wwan_ok ? C_TEXT : C_DIM, s);
/* row 5: battery (VADC VBAT from the B10 supervisor; no SOC) */
if (t.vbat_mv > 0) snprintf(s, sizeof s, "BAT %d.%02dV", t.vbat_mv / 1000, (t.vbat_mv % 1000) / 10);
else snprintf(s, sizeof s, "BAT --");
text(0, 80, t.vbat_mv > 0 ? C_TEXT : C_DIM, s);
const char *cs = t.chg[0] ? t.chg : (t.usb == 1 ? "USB" : "--");
uint32_t cc = !strcmp(cs, "FAULT") ? C_BAD : (!strcmp(cs, "CHARGING") ? C_WARN : C_OK);
if (!strcmp(cs, "CHARGING")) cs = "CHG";
snprintf(s, sizeof s, "%.5s", cs); textr(80, cc, s);
/* row 6: PMIC temperature + router (default route) */
if (t.tempc > -1000) { snprintf(s, sizeof s, "PMIC %d\xf8""C", t.tempc); text(0, 96, t.tempc >= 60 ? C_BAD : C_TEXT, s); }
else text(0, 96, C_DIM, "PMIC --");
textr(96, t.defroute ? C_OK : C_DIM, t.defroute ? "NAT" : "---");
/* row 7: clock + overall */
time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm);
if (tm.tm_year + 1900 >= 2026) { snprintf(s, sizeof s, "%02d%c%02d", tm.tm_hour, (now & 1) ? ':' : ' ', tm.tm_min); text(0, 112, C_TEXT, s); }
else text(0, 112, C_DIM, "--:--");
textr(112, online ? C_OK : (t.mstart < 0 ? C_BAD : C_WARN), online ? "ONLINE" : (t.mstart < 0 ? "FAIL" : "WAIT"));
}
static void on_sig(int s) { (void)s; quit = 1; }
int main(void)
{
setvbuf(stdout, NULL, _IOLBF, 0); setvbuf(stderr, NULL, _IONBF, 0);
struct timespec t0; clock_gettime(CLOCK_MONOTONIC, &t0);
signal(SIGTERM, on_sig); signal(SIGINT, on_sig); signal(SIGPIPE, SIG_IGN);
if (SDL_Init(SDL_INIT_VIDEO)) { fprintf(stderr, "aurora-ui: SDL_Init: %s\n", SDL_GetError()); return 1; }
const char *drv = SDL_GetCurrentVideoDriver();
printf("aurora-ui: SDL %d.%d.%d video driver %s\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL, drv ? drv : "?");
if (!drv || strcmp(drv, "wayland")) { fprintf(stderr, "aurora-ui: refusing to run on video driver '%s' (Wayland required)\n", drv ? drv : "?"); SDL_Quit(); return 2; }
SDL_Window *win = SDL_CreateWindow("aurora-ui", 0, 0, W, H, SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS);
if (!win) { fprintf(stderr, "aurora-ui: window: %s\n", SDL_GetError()); SDL_Quit(); return 1; }
SDL_ShowCursor(SDL_DISABLE);
SDL_Renderer *r = SDL_CreateRenderer(win, -1, 0);
if (!r) { fprintf(stderr, "aurora-ui: renderer: %s\n", SDL_GetError()); SDL_Quit(); return 1; }
SDL_RendererInfo ri; SDL_GetRendererInfo(r, &ri);
SDL_Texture *tex = SDL_CreateTexture(r, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, W, H);
int ww, wh; SDL_GetWindowSize(win, &ww, &wh);
printf("aurora-ui: renderer %s, window %dx%d\n", ri.name, ww, wh);
pthread_t th; pthread_create(&th, NULL, worker, NULL);
Uint32 last = 0; int first = 1;
while (!quit) {
SDL_Event e;
if (SDL_WaitEventTimeout(&e, 250)) {
if (e.type == SDL_QUIT) quit = 1;
if (e.type == SDL_WINDOWEVENT) dirty = 1;
}
Uint32 now = SDL_GetTicks();
if (now - last >= 1000) { last = now; fast(); dirty = 1; }
if (dirty) {
dirty = 0;
render();
SDL_UpdateTexture(tex, NULL, fb, W * 4);
SDL_RenderClear(r);
SDL_RenderCopy(r, tex, NULL, NULL);
SDL_RenderPresent(r);
if (first) {
struct timespec t1; clock_gettime(CLOCK_MONOTONIC, &t1);
printf("aurora-ui: first frame after %.3f s\n", (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9);
first = 0;
}
}
}
pthread_join(th, NULL);
SDL_DestroyTexture(tex); SDL_DestroyRenderer(r); SDL_DestroyWindow(win); SDL_Quit();
printf("aurora-ui: exit\n");
return 0;
}