Chaquopy's pip --find-links argument was built with a hard-coded
"file:///" prefix plus rootDir.absolutePath. That works on Windows
(paths start with "C:/...", so prefix + path produces three slashes
after "file:") but breaks on Linux (paths start with "/workspace/...",
so prefix + path produces four slashes — pip then parses "workspace"
as the URL's hostname and aborts with
"ValueError: non-local file URIs are not supported on this platform".
Pick the prefix based on whether the absolute path already starts with
"/", so we always end up with exactly three slashes between "file:" and
the drive letter or root.
Boot-time startup so LedGrab has display capture and control without user
interaction on rooted TV boxes. Also folds in a batch of review findings
from the Android package audit.
Autostart
- BootReceiver fires on BOOT_COMPLETED / LOCKED_BOOT_COMPLETED /
MY_PACKAGE_REPLACED, gated by AutostartPrefs and Root.looksRooted().
Dispatches CaptureService.createRootIntent via
ContextCompat.startForegroundService. Unrooted devices are a no-op
because MediaProjection consent cannot be bypassed silently.
- AutostartPrefs: thin SharedPreferences wrapper, defaults to enabled.
Exposed as a CheckBox on the stopped panel; greyed out when not rooted.
- Manifest: RECEIVE_BOOT_COMPLETED, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
WAKE_LOCK permissions + the new BootReceiver.
- MainActivity prompts for battery-optimization exemption on first opt-in
so Doze/App Standby doesn't kill the FG service on phones.
Service stability
- onStartCommand now flips isRunning only after startForeground succeeds
(was stuck=true forever if the FG transition threw) and resets on
exception. Returns START_REDELIVER_INTENT for root mode so the OS can
restart the service with the original intent (no consent token to
invalidate); MediaProjection mode keeps START_NOT_STICKY.
- Watchdog coroutine monitors RootScreenrecord.framesDelivered. Respawns
the pipeline on stall (reusing the existing Python bridge — no server
restart), caps at 3 consecutive restarts before giving up.
- RootScreenrecord.framesDelivered is now an AtomicInteger, exposed as a
public property for the watchdog.
- ScreenCapture takes an onProjectionStopped lambda; when the user taps
the system Cast/Screen-capture stop banner, the whole service is torn
down instead of leaving a stale FG notification.
- MainActivity's two startForegroundService calls switch to
ContextCompat.startForegroundService, clearing pre-existing NewApi lint
errors (minSdk=24 < API 26 native method).
Build
- versionCode derived from git rev-list --count HEAD (or the
ANDROID_VERSION_CODE env var for CI). Was pinned to 1 — sideload
upgrades were silently refusing to install.
- New i18n strings (autostart_label, autostart_unavailable, version_prefix)
in en/ru/zh; version_text now uses the resource instead of string
concat.
TODO.md: new "Android Autostart on Boot" section tracking done/pending
items; real-hardware verification on a Magisk'd TV box is the remaining
checkbox.
End-to-end BLE streaming: provider + client + per-protocol wire encoders
with whole-strip averaging, desktop (bleak) and Android (Kotlin BleBridge
via Chaquopy) transports, discovery with protocol-family detection that
auto-fills the UI, throttled not-connected warning + 10 s reconnect
cooldown so a dropped link no longer stalls the pipeline at ~30 s/frame,
and an explicit asyncio.wait_for wrapper around bleak connect() since
the WinRT backend doesn't always honor the timeout kwarg.
Also rewrites server/restart.ps1 to be parameterized (-Port / -Module /
-PythonVersion / timeouts / -Quiet), pick the right interpreter via the
py launcher, pre-flight the target module, poll port readiness on both
shutdown and startup, redirect child stdout/stderr so Start-Process
doesn't hang on inherited Git-Bash handles, and return proper exit codes.
Rolls in concurrent work: Android BLE permissions + launcher icons + ru/zh
resources, Chaquopy-safe value_stream psutil fallback, setup-required
modal, asset-store test coverage, and misc system/config touch-ups.
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>