| … | … | @@ -0,0 +1,137 @@ |
| 1 | # Cthuloid Lights for Android — design |
| 2 | |
| 3 | Status: **DRAFT for approval** (2026-10-02). Built before phases 3–4 of |
| 4 | `DESIGN-collab.md`, at the user's request. Lives in `~/cthuloid/android/`. |
| 5 | |
| 6 | ## 1. What v1 is |
| 7 | |
| 8 | The phone runs **the same controller as stortron** (`webui/`) inside the app, and |
| 9 | shows **the same page** in a WebView. It talks to the lamps directly over TCP, so it |
| 10 | works with stortron down. |
| 11 | |
| 12 | | | v1 (this design) | later | |
| 13 | |---|---|---| |
| 14 | | Drive lamps on the LAN (grab, drag, picker, swatches, looks, blackout) | yes | | |
| 15 | | Shares slots fairly with the web UI, other phones, Blender (firmware proto 3) | yes — each phone is its own holder | | |
| 16 | | Saved looks / swatches shared with other devices | no — on the phone | phase 3 (autobee) | |
| 17 | | Control from off the LAN | no | phase 4 (Hyperswarm relay) | |
| 18 | |
| 19 | ## 2. Shape |
| 20 | |
| 21 | ``` |
| 22 | React Native app (android/app) Bare worklet (android/lib) |
| 23 | ├─ WebView → http://127.0.0.1:<port> ───HTTP───► cthuloid-web controller (file:../webui) |
| 24 | ├─ Lamp finder (Kotlin, NsdManager) ───IPC────► ├─ Discovery adapter (units from IPC) |
| 25 | └─ lifecycle (suspend/resume) ───IPC────► └─ TCP 1337 ──► lamps |
| 26 | ``` |
| 27 | |
| 28 | - **One controller codebase.** `webui/` gets a small refactor: `createApp({ configDir, |
| 29 | host, port, discovery })` in a library module, used by `webui/index.js` on stortron |
| 30 | and by the phone's worklet. Rig, Unit, protocol, server and page are shared byte for |
| 31 | byte; no copy to drift. |
| 32 | - **Page files embedded.** A build step generates `webui/lib/assets.js` holding the |
| 33 | static files as strings. On stortron the server keeps reading `static/` from disk; in |
| 34 | the bundle it uses the embedded copy, because a packed worklet has no files on disk. |
| 35 | - **HTTP on loopback only**, port reused across restarts so the WebView URL survives. |
| 36 | The `x-cthuloid` POST guard stays. |
| 37 | - **Storage:** app-private dir; the phone's own `config.json` (units, looks, settings). |
| 38 | |
| 39 | ## 3. Finding lamps (no avahi on Android) |
| 40 | |
| 41 | - **NsdManager** (Android's own mDNS, no multicast lock needed) browses |
| 42 | `_cthuloid._tcp` in a small Kotlin module, resolves each unit (address, port, TXT |
| 43 | `proto`) and sends `unit-up` / `unit-down` to the worklet over IPC, the same events |
| 44 | `avahi-browse` produces on Linux. Resolve uses `registerServiceInfoCallback` on |
| 45 | Android 14+ and `resolveService` below it (minSdk 29). |
| 46 | - **Remembered addresses:** the last address of every unit is saved, so a known lamp |
| 47 | is reachable at once even before NSD answers. |
| 48 | - **Add by IP** (already in the page) for older firmware. |
| 49 | - Never DNS: the lamps are dialled by IP (Android gives apps no resolver config — |
| 50 | favela memory, 2026-08-24). |
| 51 | |
| 52 | ## 4. Lifecycle |
| 53 | |
| 54 | - App to background → worklet suspended → its sockets drop → each lamp frees that |
| 55 | phone's grants on disconnect (0.02 s, firmware proto 3). Resume → reconnect, adopt |
| 56 | what the lamps show (the lamp is the truth), nothing pushed. |
| 57 | - The Connect switch is session state, as everywhere: a cold start opens disconnected. |
| 58 | - Shell ↔ worklet contract through **files in storage, never positional arguments**: |
| 59 | the APK and an OTA'd bundle can be different versions (favela's argv trap). |
| 60 | |
| 61 | ## 5. Build, sign, install |
| 62 | |
| 63 | - Stack from **favela-surface-android** (proven on hardware): react-native 0.86 + |
| 64 | react-native-bare-kit 0.14.5 + react-native-webview 14, minSdk 29, arm64-v8a only, |
| 65 | NDK 27.1, JDK 17, Gradle 8.13 from `~/android-toolchain`. |
| 66 | - Worklet packed with `bare-pack --preset android`; only the addons it demands are |
| 67 | staged, and CI verifies every shipped `.so`'s NEEDED entries (favela's |
| 68 | `verify-apk-addons.mjs` — a missing dependency otherwise crash-loops on the phone, |
| 69 | reported against the wrong library). |
| 70 | - **Signing key:** a new keystore, `~/android-toolchain/cthuloid.keystore`, **never in |
| 71 | git**, kept for good: Android refuses an update signed with another key, and the only |
| 72 | way out is uninstall, which wipes the app's data. |
| 73 | - `applicationId` **`org.circusautomatic.cthuloid`** (the GitHub org), label |
| 74 | **"Cthuloid Lights"**. |
| 75 | - **CI builds on `android-v*` tags only** (D-A2). The forge runs `.pear-git/ci.yaml` |
| 76 | for every pushed ref and can't filter, but each step gets `GIT_REF_KIND` and |
| 77 | `GIT_REF`, so every step does nothing unless the ref is a tag matching |
| 78 | `android-v<semver>`. The artifact is the directory `dist/` (artifacts may be |
| 79 | directories): on a tag it holds `cthuloid-lights.apk` + `cthuloid-lights.apk.sha256`; |
| 80 | otherwise only a one-line `skipped.txt`. (A listed-but-missing file would throw |
| 81 | in `collectArtifacts` and leave a misleading error in the forge log.) Release a |
| 82 | version by pushing the tag: `git tag android-v0.1.0 && git push origin android-v0.1.0` |
| 83 | — the version comes from the tag, never from hand-editing a field. |
| 84 | - Download: `https://7rnx.net/git/releases/cthuloid/android-v<x.y.z>/dist/cthuloid-lights.apk` |
| 85 | (first install by sideload; "install unknown apps" for the browser). |
| 86 | |
| 87 | ## 6. Updates — the app fetches the newest APK from the forge (D-A1) |
| 88 | |
| 89 | No over-the-air JS (favela's worklet-bundle OTA is 14 modules of hard-won lifecycle |
| 90 | code; not in v1). Instead the app updates **itself as a whole APK**, over plain HTTPS, |
| 91 | in the React Native layer — Android's own networking, so the Bare-side DNS problem |
| 92 | does not apply: |
| 93 | |
| 94 | 1. **Check** on start and every 6 h while open: `POST https://7rnx.net/git/rpc/release-index` |
| 95 | (anonymous-allowed; the other listing method returns 403 to a phone, which favela |
| 96 | once read as "up to date"). Keep `repo == "cthuloid"` files whose version matches |
| 97 | `android-v<semver>`. |
| 98 | 2. **Only complete releases:** a version counts only if both `cthuloid-lights.apk` and |
| 99 | `.sha256` are listed — the two forge backends replicate files one by one, and an |
| 100 | APK once never reached the second (favela, 2026-08-18). |
| 101 | 3. **Newest by semver** from the tag (never by list position — the forge lists newest |
| 102 | first, and favela's first cut picked the oldest), compared with the installed |
| 103 | `versionName`. |
| 104 | 4. If newer: a banner above the page — "Update 0.1.1 available — Install". Tapping it |
| 105 | downloads to app cache, checks the SHA-256, then hands the file to Android's |
| 106 | installer (`FileProvider` + install intent, `REQUEST_INSTALL_PACKAGES`). The first |
| 107 | time, Android asks to allow "install unknown apps" for Cthuloid Lights; every |
| 108 | install still needs the user's tap. A failed or partial download is discarded and |
| 109 | retried at the next check. |
| 110 | 5. **Safety:** Android only accepts the update if it is signed with the same key, so a |
| 111 | tampered APK cannot replace the app; the checksum guards against a truncated |
| 112 | download. Data is kept across updates. |
| 113 | |
| 114 | ## 7. Testing |
| 115 | |
| 116 | - Worklet logic: the existing webui suites (shared code) + a Discovery-adapter test. |
| 117 | - Kotlin lamp finder: exercised on a real phone against studiolight (NSD can't be |
| 118 | faked on the host). |
| 119 | - End to end on a phone over adb: install, discover studiolight, grab a slot, check the |
| 120 | web UI on stortron shows "held by <phone>", background the app, check the slot frees. |
| 121 | |
| 122 | ## 8. Build order (each verified before the next) |
| 123 | |
| 124 | 1. `webui` refactor: `createApp`, embedded assets, injectable discovery (tests stay green). |
| 125 | 2. Android skeleton: RN + bare-kit + WebView; worklet serves the page from the APK |
| 126 | (manual add by IP works). |
| 127 | 3. Lamp finder (NsdManager) + remembered addresses. |
| 128 | 4. CI (tag-only) + keystore + first published APK; on-phone e2e. |
| 129 | 5. Self-update: check, banner, download + checksum, installer hand-off; verified by |
| 130 | publishing `android-v0.1.1` and updating a phone that runs 0.1.0. |
| 131 | |
| 132 | ## 9. Decisions for the user |
| 133 | |
| 134 | - **D-A1** ✅ No over-the-air JS; the app self-updates as a whole APK from the forge (§6). |
| 135 | - **D-A2** ✅ Build on `android-v*` tags only (§5). |
| 136 | - **D-A3** ✅ `org.circusautomatic.cthuloid` / "Cthuloid Lights". |
| 137 | - **D-A4** ⏳ Which phone to test on (none is attached to tabertron over adb right now). |