OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid commits / f582878

docs: Android app design (decisions D-A1..3)
Phone runs the same controller as stortron (webui) in a bare-kit worklet with the same page in a WebView; finds lamps with NsdManager; no OTA JS, instead a self-update that fetches the newest complete android-v* APK from the forge over HTTPS and hands it to Android's installer; CI builds on android-v* tags only (steps no-op on other refs, dist/ artifact dir). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f58287872cd7ace4929fd55d721f5f133c8f438f
by plankatron <fleeky@gmail.com> · 2026-10-02T15:47:20.000Z

Changed files (1) +137 -0

docs/DESIGN-android.md +137 -0
……@@ -0,0 +1,137 @@
1# Cthuloid Lights for Android — design
2
3Status: **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
8The phone runs **the same controller as stortron** (`webui/`) inside the app, and
9shows **the same page** in a WebView. It talks to the lamps directly over TCP, so it
10works 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```
22React 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
89No over-the-air JS (favela's worklet-bundle OTA is 14 modules of hard-won lifecycle
90code; not in v1). Instead the app updates **itself as a whole APK**, over plain HTTPS,
91in the React Native layer — Android's own networking, so the Bare-side DNS problem
92does not apply:
93
941. **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>`.
982. **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).
1013. **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`.
1044. 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.
1105. **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
1241. `webui` refactor: `createApp`, embedded assets, injectable discovery (tests stay green).
1252. Android skeleton: RN + bare-kit + WebView; worklet serves the page from the APK
126 (manual add by IP works).
1273. Lamp finder (NsdManager) + remembered addresses.
1284. CI (tag-only) + keystore + first published APK; on-phone e2e.
1295. 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).