On rooted TV boxes, spawn `su -c screenrecord ... -` and feed the
H.264 stdout through MediaCodec into an ImageReader, surfacing RGBA
frames via PythonBridge. RootScreenrecordEngine (priority 110) is
picked automatically when root is available; falls back to
MediaProjection when Root.requestGrant() returns false.
Adds end-to-end support for driving USB-connected Adalight / AmbiLED
LED controllers from Android TV boxes. Android's security model blocks
direct USB access from Python, so writes route through a Kotlin
UsbSerialBridge singleton via Chaquopy.
Python side:
- New SerialTransport Protocol (serial_transport.py) with open / write /
flush / close. Desktop uses PySerialTransport (wraps pyserial),
Android uses AndroidSerialTransport (wraps the Kotlin bridge).
- list_serial_ports() factory returns desktop COM ports on desktop,
USB devices on Android — callers don't branch.
- URL scheme extended: existing COM3[:baud] and /dev/ttyUSB0[:baud]
unchanged; new usb:VID:PID[:serial][@baud] for Android (@ is the
baud separator since : is already used between VID and PID).
- AdalightClient and SerialDeviceProvider refactored to go through
the transport — no more direct pyserial imports in hot paths.
- 17 new unit tests cover URL parsing, PySerial transport, factory
selection, platform-branching discovery. Full suite 750 passing.
Kotlin side:
- UsbSerialBridge.kt singleton uses com.hoho.android.usbserial (mik3y)
which ships drivers for CH340, CP2102, FTDI, Prolific, and CDC-ACM
(Arduino). Exposes listDevices, open, write, close via @JvmStatic
for Chaquopy. First open() attempt without permission triggers the
system USB permission dialog; next call succeeds once user grants.
- usb-serial-for-android is distributed via JitPack — added that repo
in settings.gradle.kts and the dependency in app/build.gradle.kts.
- AndroidManifest declares uses-feature android.hardware.usb.host
(required=false so non-USB-host phones still install).
- LedGrabApp.onCreate calls UsbSerialBridge.init(this) so the bridge
resolves the UsbManager without needing an Activity ref.
Verified: ./gradlew compileDebugKotlin succeeds; off-Android import
of android_serial_transport works. Real-hardware smoke test on a
TV box with a CH340/CP2102/FTDI adapter still pending.
ESP-NOW (espnow_client / espnow_provider) still imports pyserial
directly because it needs bidirectional reads — separate refactor
to extend the transport with read() if that path ever needs Android
USB support.
Adds .gitea/workflows/build-android.yml — Linux runner installs JDK 17,
Python 3.11, Android SDK/NDK, symlinks server/src/ledgrab into the
Chaquopy python source dir, and runs assembleDebug on master pushes /
assembleRelease on v* tags. APK is uploaded as an artifact and attached
to the Gitea release on tag push. Conditional signing config in
build.gradle.kts reads keystore from env vars (CI secrets) and falls
back to debug signing locally. Gradle wrapper (gradlew/gradlew.bat/
gradle-wrapper.jar) committed so CI can drive the build.
Rebuilds pydantic-core wheels for arm64-v8a and x86_64 — both were
missing libpython3.11.so in NEEDED, which would have crashed at import
on real devices. build-pydantic-core.sh rewritten as a multi-ABI builder:
selects targets via args, sets RUSTFLAGS=-C link-arg=-Wl,--no-as-needed
-C link-arg=-lpython3.11 to force the symbol-resolution dependency,
uses the per-ABI sysconfigdata + libpython staged in
android/.build-cache/, prefers `py -3.11` on Windows (Git Bash's
python3.11 is an MSStore stub), uses the .cmd clang wrapper on Windows
(fixes os error 193), and verifies NEEDED via llvm-readelf after each
build. abiFilters restored to the full triple in build.gradle.kts;
multi-ABI debug APK builds cleanly (~99 MB).
Adds a native Android TV application that runs the full LedGrab Python
server in-process via Chaquopy. Captures the TV box screen using the
MediaProjection API and exposes the existing web UI on the device's
local network — users configure via phone/tablet browser.
Android (new /android/ module):
- Kotlin shell: MainActivity, CaptureService (foreground service),
ScreenCapture (MediaProjection + ImageReader), PythonBridge (Chaquopy).
- Polished Leanback-themed UI with QR code for easy web UI access.
- AGP 8.9 + Chaquopy 17 + Gradle 8.11 (avoids the AGP 8.7 thread-lock bug).
- Pre-built pydantic-core wheels for arm64-v8a, x86_64, x86 cross-compiled
with maturin + Android NDK, linked against Chaquopy's libpython3.11.so.
Python server platform guards:
- New utils/platform.py with is_android()/is_windows()/is_linux() helpers.
- Guard every top-level import of desktop-only packages (mss, psutil,
sounddevice, pyserial, PyAudioWPatch, etc.) with try/except ImportError.
- Android-incompatible calls gated with None-checks so the server runs on
reduced capabilities on Android (no CPU/RAM metrics, no mss displays).
- utils/image_codec.py gains a Pillow fallback for resize + JPEG encode
when cv2 is unavailable; all internal cv2.resize callers migrated.
- New android_entry.py start_server/stop_server invoked from Kotlin.
- get_displays API falls back to best available engine when mss fails.
New capture engines:
- MediaProjectionEngine: receives RGBA frames pushed from Kotlin through
a thread-safe queue; caches last frame for static-screen previews.
- ScrcpyClientEngine: optional H.264 streaming via scrcpy-client library
(priority 10, overrides the ADB-screencap engine when installed).
Frontend:
- Tab loaders previously required an apiKey; now correctly treat
"auth disabled" as authenticated (Android has no auth by default).
- Re-trigger the active tab's loader after loadServerInfo resolves
authRequired, since initTabs runs earlier.
- Add i18n keys for the demo / mediaprojection / scrcpy_client engines.
Docs:
- TODO.md: follow-ups for multi-ABI wheel rebuilds, CI pipeline, USB
serial LED controllers, root-only capture, perf metrics abstraction.
- CLAUDE.md: Android dependency sync policy (pip --exclude doesn't exist).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>