Files
ledgrab/TODO.md
T
alexei.dolgolyov ecae05d00b
Build Android APK / build-android (push) Failing after 1m40s
Lint & Test / test (push) Successful in 4m18s
feat(metrics): battery + thermal-zone readings with dashboard temp chart
Extends MetricsProvider with thermals() returning a ThermalSnapshot
(battery_percent, battery_temp_c, cpu_temp_c — all optional). Each
provider implements it independently:

- AndroidMetricsProvider reads /sys/class/power_supply/battery/{capacity,
  temp} (battery temp is tenths of degC) and walks
  /sys/class/thermal/thermal_zone*, filtering by zone type
  (cpu/soc/tsens/core) so battery and skin sensors don't dominate the
  reading. Rejects nonsense values like INT_MAX from buggy zones.
- PsutilMetricsProvider uses sensors_battery() and
  sensors_temperatures() when present (Linux+laptops); no-ops on
  Windows/macOS where psutil doesn't expose them.
- NullMetricsProvider returns the empty snapshot.

PerformanceResponse gains battery_percent / battery_temp_c / cpu_temp_c.
The metrics-history ring buffer also carries cpu_temp / battery_pct /
battery_temp per sample so the dashboard can graph them over time.

Frontend dashboard (perf-charts.ts) gets a new Temperature chart card,
hidden by default and revealed only after seed/poll confirms the
backend reports cpu_temp_c. Battery temperature shows inline as a
secondary badge. The GPU card now also hides entirely when the backend
reports gpu=null instead of showing an "unavailable" placeholder.
HOST_ONLY_KEYS prevents the System/App/Both toggle from flipping a
non-existent app dataset for temp.

Tests: 6 new for thermals (battery tenths-of-degC parsing, CPU zone
filtering, fallback when sensors absent, INT_MAX rejection); 18 metrics
tests total; full suite 733 passing.
2026-04-14 13:48:01 +03:00

6.0 KiB

LedGrab TODO

Android — Restore Multi-ABI Wheels

During emulator testing, we switched the build to x86 only (see android/app/build.gradle.kts abiFilters) to avoid having to keep the arm64-v8a / x86_64 pydantic-core wheels current. Before shipping, restore all three ABIs:

  • Rebuild pydantic-core wheels for all three ABIs with the current SOABI + libpython linking settings (android/build-scripts/build-pydantic-core.sh — now supports arm64, x86_64, x86 args; defaults to all three).
  • Verify wheels: all three now list libpython3.11.so in NEEDED (llvm-readelf -d), automated in the build script.
  • Restored abiFilters += listOf("arm64-v8a", "x86_64", "x86") in build.gradle.kts. Multi-ABI debug APK builds cleanly (~99 MB).
  • Re-test on real ARM64 Android TV hardware (still pending — only emulator-verified build).

Build cache + scripts live in android/build-scripts/ and android/.build-cache/ (junction host + sysconfigdata for each ABI).

Android CI Pipeline

Build the Android APK automatically on push/tag.

  • Generate Gradle wrapper (gradlew) and commit it
  • Create CI workflow (.gitea/workflows/build-android.yml)
    • JDK 17 + Android SDK + NDK setup
    • Python 3.11 for Chaquopy build
    • Recreate the directory junction via ln -s on Linux CI
    • ./gradlew assembleDebug on master push, assembleRelease on v* tags (if signing secrets set)
    • Uploads APK as CI artifact; attaches to Gitea release on tag push
  • Commit pre-built pydantic-core wheels to android/wheels/ (arm64, x86, x86_64)
  • APK signing for release builds — conditional signing config reads keystore from env vars (ANDROID_KEYSTORE_PATH/_PASSWORD/_ALIAS/_KEY_PASSWORD), falls back to debug signing locally
  • Provision a real keystore and add the four CI secrets:
    • ANDROID_KEYSTORE_BASE64 (base64-encoded .jks)
    • ANDROID_KEYSTORE_PASSWORD
    • ANDROID_KEY_ALIAS
    • ANDROID_KEY_PASSWORD
  • Add LedGrab-{tag}-android-release.apk row to the release description table in .gitea/workflows/release.ymlcreate-release job
  • Verify the CI workflow passes end-to-end with the now-restored multi-ABI build (larger APK, longer Android build step)

Android Root Capture (No Permission Dialog, No System Indicator)

MediaProjection shows a mandatory system overlay/indicator while capturing — unavoidable on stock Android. Many cheap Android TV boxes ship pre-rooted, so an alternative root-only path would give much better UX.

  • Detect root at runtime: check for su binary, Superuser.apk, etc.
  • Implement SurfaceControlCaptureEngine (new capture engine) using hidden SurfaceControl.screenshot() API via reflection
    • No permission dialog
    • No system capture indicator
    • Direct bitmap output (no encoder/decoder roundtrip)
  • Engine priority: higher than MediaProjection when root detected
  • Fallback chain: SurfaceControl (root) → MediaProjection (stock) → adb screencap (last resort)
  • Handle Android version differences in SurfaceControl API surface (renamed/moved across API 29, 30, 33)
  • Alternative: shell out to screenrecord --output-format=h264 - as root (same H.264 decode as scrcpy_client_engine, but local instead of remote ADB)

Known projects using this approach for reference: scrcpy-hidden-api, shizuku, commercial scrcpy-derived apps.

Android USB Serial Support

Drive USB LED controllers (APA102, WS2812) connected directly to the Android TV box via USB-to-serial adapters.

  • Add usb-serial-for-android dependency to android/app/build.gradle.kts
  • Create Kotlin UsbSerialBridge class that:
    • Enumerates USB serial devices via Android USB Host API
    • Requests user permission for USB device access
    • Opens a serial connection (baud rate configurable)
    • Exposes a write method callable from Python via Chaquopy
  • Create Python AndroidSerialProvider in server/src/ledgrab/core/devices/ that:
    • Replaces pyserial on Android (which can't access USB ports)
    • Calls UsbSerialBridge via Chaquopy to send LED data
    • Registers as an alternative serial transport when is_android() is True
  • Add USB device permission dialog to MainActivity (auto-triggered on device connect)
  • Test with common USB-to-serial chips: CH340, CP2102, FTDI
  • Document supported USB LED controllers in README

Performance Metrics Abstraction

  • MetricsProvider protocol + dataclass DTOs (MemorySnapshot, ProcessSnapshot) live in server/src/ledgrab/utils/metrics/types.py. Each provider has its own module: psutil_provider.py, null_provider.py, android_provider.py.
  • Factory get_metrics_provider() in utils/metrics/__init__.py selects Android → psutil → Null. psutil import is now confined to one place.
  • api/routes/system.py and core/processing/metrics_history.py use the provider; no more if psutil is not None guards in the hot paths.
  • Android /proc-backed provider implemented (/proc/stat, /proc/meminfo, /proc/self/stat, /proc/self/status). Carries previous-sample state for delta-based CPU%; degrades to zeros if any /proc file is locked down. 12 unit tests cover both desktop and Android paths.

Android Performance Metrics — Future Enhancements

Beyond the /proc-based AndroidMetricsProvider that's now in place:

  • Device battery + thermal-zone readings (/sys/class/power_supply/battery/{capacity,temp}, /sys/class/thermal/thermal_zone*/temp filtered by zone type). Surfaced through MetricsProvider.thermals(), PerformanceResponse.{cpu_temp_c,battery_percent,battery_temp_c}, the metrics-history snapshot, and a new dashboard temperature chart that hides itself when the backend reports null. GPU card now hides (no "unavailable" placeholder) when no GPU is present.
  • [WONTDO] Optional: app-specific memory via Debug.getMemoryInfo() through a Kotlin → Python Chaquopy bridge (more accurate than VmRSS for split-app-process accounting)
  • [WONTDO] Optional: GPU usage via /sys/class/kgsl/kgsl-3d0/gpubusy on Adreno, Mali-specific paths for Mali GPUs