379 lines
14 KiB
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;
|
|
}
|