multinetwork now SUPPORTED (wifi+cellular bound concurrently; the cellular request surfaced a second dual-stack PDN), Shizuku battery 7/7 in 2.9s (was 4/7 in 34s), rtt_ms locale-clean. Remaining probe improvements (per-network ping6, longer mDNS/ip-monitor windows) noted in build-status. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.8 KiB
Echolot — build status & next steps
Last updated: 2026-07-29.
Decided
- Name Echolot; domain echo-lot.app; scheme
echolot://; namespaceapp.echo_lot.*(hyphen→underscore; appIds/packages can't contain hyphens). Prober appIdapp.echo_lot.prober. - Stack: native Kotlin + Jetpack Compose, no Flutter.
- Tiers:
app(no root),shizukuin v1 (wireless-ADB pairing),rootfuture. - License (decided 2026-07-30): all code GPL-3.0-or-later; the specs in
docs/CC-BY-4.0. Rationale: the moat is the no-root platform research, which is trivially liftable into a proprietary repackage — copyleft is the only option that prevents that, and the F-Droid / network-engineer audience reads GPL as a trust signal rather than friction. The specs go permissive on purpose: a wire protocol only becomes a standard if anyone can implement it. Not AGPL for the server — the ASP loophole it closes is speculative here, while blanket corporate AGPL bans would hit exactly the enterprise network teams most likely to self-host. Sole copyright holder, so relicensing the server to AGPL later stays possible. Open: substitute a real legal copyright holder for "Echolot contributors" in the SPDX headers.
Specs (in docs/, alongside this file)
feature-catalog-and-feasibility.md,measurement-schema.md,probe-protocol.md. Considered draft-complete and reviewed by the user.
Capability prober — DELIVERED (as zip, 2026-07-29)
Full Kotlin/Compose project scaffolded: app.echo_lot.prober, minSdk 26 / target+compile 35,
AGP 8.7.3, Kotlin 2.0.21, Compose BOM 2024.10, Shizuku api+provider 13.1.5, kotlinx-serialization.
Probes implemented: link.snapshot, icmp.ping4/6 (unprivileged ICMP datagram), sockopt.matrix
(TTL/TOS/RECVERR/MTU_DISCOVER), trace.errqueue_reachable, multinetwork.request_and_bind,
local.mdns_discover, peer.ble_advertise, shizuku.command_battery (ip neigh / ip -6 route /
ip addr / ip monitor / dumpsys network_stack DHCP+IpClient / dumpsys wifi). JSON export via share
intent; results carry verdict + raw evidence.
Could NOT be compiled in the cloud sandbox: dl.google.com (Google Maven) and services.gradle.org are proxy-blocked, and no device is reachable for on-device runs. Build + iterate locally (Android Studio / Claude Code). Wrapper is pinned to Gradle 8.14.3.
Website — web/ (2026-07-30, LIVE at https://echo-lot.app)
Single-page site for echo-lot.app on Cloudflare Workers: static public/ from the edge, a small
Worker for the stable short URLs (/apk, /apk.sha256, /fdroid, /source) and /api/latest.
Version info is resolved from the Gitea releases API at request time (edge-cached 5 min), so
tagging a release is the only publish step — the homepage shows the latest version/date/size via
progressive-enhancement JS and falls back to pre-release copy otherwise. Auto light/dark via
prefers-color-scheme: dark = sonar display, light = chart-recorder printout, same amber-accent
token system. Deploy: npx wrangler deploy from web/, or CI via
.gitea/workflows/deploy-site.yml (needs CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID
secrets); full story in web/README.md. Branding applied (2026-07-30): real favicon/wordmark
copied into web/public/assets/ (copies — assets/branding/ is the source of truth), palette
switched to abyss/teal/amber with teal-as-instrument semantics, sonar-sweep hero replaced by a
Focus-lattice panel (rogue-DHCP finding vignette), tagline "measure, don't guess".
Branding — assets/branding/ (2026-07-30)
Identity chosen: the Focus mark — a quiet lattice of nodes with exactly one under examination
(amber) inside teal viewfinder brackets. Tone is surgical measurement, not scanning (sonar-sweep
visuals and the "sound out your network" tagline were explicitly rejected); tagline is
"measure, don't guess". Palette: abyss #071522, tile #0E2433, instrument teal #35E0C4,
finding amber #FFB454, foam #E8F4F2 — teal is always the instrument, the single amber point is
the finding. The wordmark is hand-drawn monoline SVG paths (zero font dependency); the second "o"
is the signature teal ring + amber ping. Assets: icon.svg (+ icon.png 512² — Gitea avatars reject SVG), icon-adaptive-{foreground,background}.svg
(108dp adaptive-icon layers, art inside the 66dp safe circle), wordmark-on-{dark,light}.svg,
banner.svg (1200×300, wired into the repo README), social-preview.svg/.png (1280×640, for the
Gitea/GitHub social-preview slot). The website's placeholder favicon/wordmark can now be replaced
from these.
First device report — OnePlus 15, Android 16/SDK 36 (2026-07-30)
Archived at echolot-prober/reports/CPH2747-android16-sdk36.json. Collection workflow that works:
deploy over (wireless) adb, but the user starts the run and exports the JSON manually — probe
runs reliably kill the adb-over-wifi session (multinetwork/multicast churn the link adb rides on).
OS findings:
link.snapshot,icmp.ping4(38 ms),sockopt.matrix4/4,peer.ble_advertise: SUPPORTED.trace.errqueue_reachableSUPPORTED — full Os-API errqueue path exists on Android 16; the C-over-JNI shim (next-steps #2) may be unnecessary on modern devices. Still needs a real errqueue round-trip test, and older devices may differ.icmp.ping6EAGAIN — topology, not capability: wifi was v4-only, v6 only on cellular, socket used the default network. Probe should bind per-network (also covers the ping4 case properly).shizuku.command_batterySUPPORTED, shell(2000): realip -6 routeRA data (lifetimes, per-table),ip monitorNEIGH events, wifi dump with full DhcpResults. Vendor formats look parseable.local.mdns_discover: lock acquired, discovery ran, 0 services in the 4 s window — needs a longer window / known-good reference network before calling it more than formally SUPPORTED.
Prober bugs the run exposed (all fixed same day):
- rotation wiped results + cancelled runs → state moved to a ViewModel;
multinetwork.request_and_bindERROR → missingCHANGE_NETWORK_STATEin the manifest;rtt_ms: "38,1"→ device locale leaked into report numbers, nowLocale.ROOT;- 3/7 Shizuku commands
SHIZUKU_BIND_TIMEOUT→ bind/unbind-per-command raced Shizuku; now one bind per battery (execBatch). Run 2 (same device, fixed APK —…-run2.json) confirmed all three fixes: multinetwork.request_and_bindSUPPORTED — wifi + cellular acquired and bound concurrently (downKbps 39094 / 13231); requesting cellular even brought up a second, dual-stack PDN (rmnet_data2, private v4 /30 + carrier-NAT DNS) next to the IPv6-only default PDN — exactly the per-network behavior the production app needs.- Shizuku battery 7/7 in 2.9 s (was 4/7 in 34 s): full ARP/NDP neighbor table,
ip addr, IpClient ProvisioningConfiguration incl. SSID/BSSID and APF caps (v6000, 4096 B). rtt_ms: "32.0"— locale fix holds. Still open (probe improvements, not blockers): ping6 should bind per-network (still EAGAIN via v4-only default wifi); mDNS window too short to see real services (0 found on a network that has them);ip monitor2 s window caught nothing on a quiet net — consider longer/event-triggered capture; IP_MTU read via getsockoptInt reflection unavailable — reader belongs in the errqueue shim if that lands.
Next steps
- Build locally, run on several physical devices (varied Android versions/vendors), collect the JSON reports — especially the real per-device Shizuku dump formats.
- If
trace.errqueue_reachable= PARTIAL, add a C-over-JNI errqueue shim (recvmsg+cmsg parse) as a:nativemodule and a realtraceroute.udp4probe. - Start the Go server skeleton (enrollment + profile + sessions + UDP echo with observation blocks + canary-DNS reference records) per probe-protocol.md.
- Fold confirmed capabilities into the production
core-probe/core-shizukumodules.