Files
ledgrab/TODO.md
T
alexei.dolgolyov 546b24d015
Build Android APK / build-android (push) Failing after 1m39s
Lint & Test / test (push) Successful in 4m20s
refactor(metrics): MetricsProvider abstraction with Android /proc backend
Moves direct psutil.* calls behind a MetricsProvider Protocol so the
codebase no longer needs ad-hoc `if psutil is not None` guards at every
call site. Each provider lives in its own module under
utils/metrics/: PsutilMetricsProvider for desktop, NullMetricsProvider
as a zeroed fallback, AndroidMetricsProvider that reads /proc/stat,
/proc/meminfo, /proc/self/stat, and /proc/self/status directly (psutil
isn't available under Chaquopy). The Android provider tracks the
previous CPU sample so cpu_percent() returns delta-based percentages
matching psutil's interval=None semantics, and degrades to zeros when
any /proc file is unreadable instead of crashing the dashboard.

Factory get_metrics_provider() in utils/metrics/__init__.py picks
Android > psutil > Null. api/routes/system.py and
core/processing/metrics_history.py now go through the factory; psutil
import is confined to one place. 12 new unit tests cover paren-in-comm
parsing of /proc/self/stat, delta CPU%, missing-file resilience, and
factory selection order. Full suite: 727 passing.
2026-04-14 13:34:32 +03:00

5.7 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:

  • Optional: app-specific memory via Debug.getMemoryInfo() through a Kotlin → Python Chaquopy bridge (more accurate than VmRSS for split-app-process accounting)
  • Consider: device battery/temperature readings for TV boxes (some have thermal throttling)
  • Optional: GPU usage via /sys/class/kgsl/kgsl-3d0/gpubusy on Adreno, Mali-specific paths for Mali GPUs