Remote camera setup: a lab phone you can update and reset from home

The P3 phone will sit in the AI Foundry lab on Wi-Fi and Tailscale, with no computer plugged in. Here is the infrastructure that keeps it updatable and resettable from home, and what still works when each piece fails.

Plan written from the M5 in ~/git/thermal-android; updated with what is now built. Anything not marked exists or listed under works now is still a plan.

Built by 15 Oct (v1.3.8 + Keeper v1.0.1): tap-free OTA over Tailscale, Cellular SIM & USB, automatic LTE streaming failover, and full discussion hardening

0
taps to update
verified over Tailscale, Google Fi LTE SIM, Spacesheep & USB
3
network transports
Wi-Fi, Google Fi LTE SIM, and USB-Ethernet (eth0)
1.39 A
into the battery
+28%/h on the Anker USB-C hub while streaming; 10-min unplugged alert
Works now (v1.3.8, code 11 + Keeper v1.0.1)
  • Tap-free OTA updates over Tailscale, Google Fi LTE SIM, Spacesheep & USB. ./deploy-phone.sh --http pushes the signed APK to POST /api/upgrade (`:8080`) or Keeper (`:8081`), and ./deploy-phone.sh --spacesheep stages the APK on GitHub release ota-latest and triggers upgrade_url over thermal/p3-control — both verified over Wi-Fi and over cellular LTE (`rmnet_data2`) with Wi-Fi disabled. In the lab, ./deploy-phone.sh --usb deploys straight over a USB-C cable.
  • Automatic Wi-Fi ↔ Cellular LTE ↔ USB-Ethernet failover for live streaming. Every 5 s StorageUtils.evaluateNetworkFailover() checks whether Wi-Fi has NET_CAPABILITY_VALIDATED. When Wi-Fi drops, it binds the process via ConnectivityManager.bindProcessToNetwork(cellularNetwork) and triggers an immediate WebRTC ICE reconnect (`SpacesheepPublisher.onNetworkTransportChanged`) so 25 fps thermal video, 12 Hz telemetry, and thermal/p3-control continue uninterrupted over the SIM card (and switch back to Wi-Fi or USB-Ethernet when restored).
  • Unattended Direct Boot (`LOCKED_BOOT_COMPLETED`), persistent USB permission & wireless ADB keep-alive. Both com.thermal.p3stream and com.thermal.p3keeper are directBootAware="true", hold persistent IUsbManager USB permission, and hold WRITE_SECURE_SETTINGS to re-assert adb_wifi_enabled = 1 and adb_allowed_connection_time = 0 on boot and every 30 s.
  • USB disconnect, wedged bulk EP & frozen-frame (`pictureHealthy`) self-healing. Bumping the cable fires an in-place 0x40/0xEE poke at 1.5 s / 3.0 s; a wedged bulk endpoint (EP0 alive, 0 frames) triggers kernel resetDevice() + CLEAR_FEATURE(ENDPOINT_HALT); and a 64-bit FNV-1a pixel hash (`lastFrameHash`) auto-triggers softReset() if 150 consecutive frames (~6 s) are identical.
  • Two core presets: Visualizing vs. Raw Signal (?action=preset&name=visualize|raw). One call switches the camera pipeline between calibrated °C viewing and unsmoothed 14-bit NUC raw capture (protocol guide), persisting across USB reconnects and reboots.
  • Resource, crash, exit-reason & battery diagnostics. Reports per-core CPU, scheduler wait %, RAM, free storage MB, Wi-Fi RSSI dBm, battery temperature (`battery_overheat` ≥ 40 °C), on-battery timer (`unplugged_alert` ≥ 10 min), ActivityManager.getHistoricalProcessExitReasons, and Device-Protected last_crash.json.
Closed in v1.3.6–v1.3.8 (formerly missing)
  • built The separate Keeper app (com.thermal.p3keeper v1.0.1, port 8081): 30 s health check, auto-restart after 3 misses, independent rollback/reboot/upgrade, listenPorts discovery, and kick_tailscale.
  • built Automated binary rollback & crash reporting: caches verified builds as last_good.apk after 25 s at ≥10 fps, auto-rolls back after 10 min if a new update fails, and records last_crash.json + OS exit reasons.
  • built Token auth on ports 8080, 8081 & thermal/p3-control: verified in constant time against DEFAULT_TOKEN_SHA256.
  • built Backed-up signing key: deterministic keystore Config + automatic backup at ~/.config/spacesheep/p3-signing-backup.keystore (`chmod 600`).
  • built Zero raw keys in APK: only the 64-hex SHA-256 digest is compiled in; live token lives in Device-Protected storage and is backed up to Keeper.
  • built Wireless ADB always comes back + fast USB fallback: both apps hold WRITE_SECURE_SETTINGS to keep wireless ADB on across reboots; port discovery works via :8080, Keeper :8081, or a 128-thread TCP scan (`32768–60999`), plus ./deploy-phone.sh --usb in the lab.

Verified live on the Realme 14T 5G (`RMX5074`) over Wi-Fi, Tailscale (`100.98.192.61`), Google Fi LTE SIM, and direct USB-C (`./deploy-phone.sh --usb`).

A keeper app and two independent ways in: short of a dead phone, every failure is fixed from home

2
ways to reach the phone
Tailscale, and a Spacesheep stream the phone polls outward
1
keeper app
Android's device owner: installs, restarts, reboots, rolls back
6
automatic reset rungs
before you need the smart plug or a person
0
cables to a computer
the phone's one USB-C port holds the P3 and the charger, and the phone is the USB host

Four paths into the phone, each with its own dependencies

Updates

needs: Wi-Fi, plus Tailscale or the public mirror
HomeA Mac builds and signs the APK
Update hostThe Intel over the tailnet; a public mirror
Lab phoneKeeper pulls, checks hash and signature, installs

Today, without a keeper: the Mac pushes the build over Tailscale straight to the camera app, which installs it with no tap.

Direct control

needs: Wi-Fi and Tailscale
HomeYou, or the Intel's watchdog
TailscaleYour tailnet, only your devices allowed
Keeper APIHealth, restart, reboot, update now (token)

Outbound control

needs: Wi-Fi and spacesheep.dev, not Tailscale
AnywhereA button on the camera's Spacesheep page
SpacesheepThe press waits on a stream; the heartbeat comes back
KeeperLong-polls outward, so no inbound port

Power

needs: the plug's own cloud, and a running keeper
AnywhereThe smart plug's own app
LabCuts the hub's charger for 2 minutes
KeeperSees power drop then return, reboots the phone

Inside the phone

KeeperDevice owner. Updates, reset ladder, reboots, heartbeat, holds this phone's settings
Camera appcom.thermal.p3stream: P3 driver, port 8080, WebRTC to Spacesheep; installs its own updates and starts on boot
TailscaleAlways-on VPN, key expiry off
AndroidNo screen lock, awake on charger, OS updates only at night
USB-C hubP3 and charger on the phone's one port; since 15 Oct it charges the phone, +28%/h while streaming

The keeper is a separate app on purpose: a bad camera build can't break the path that replaces it.

If something fails: what fixes it, and whether anyone has to go in

What breaksFixed by, automaticallyIf that doesn't workVisit?
P3 stream freezesCamera app: USB re-sync, then re-handshakeKeeper restarts the camera appNo
Camera app hangs or crashesKeeper restarts it within a minuteKeeper reboots the phoneNo
A bad updateKeeper's 10-minute check fails, it reinstalls the last good build"Roll back" button, either pathNo
A WebView update breaks WebRTCCamera app restarts WebRTCKeeper restarts the app, then rebootsNo
The phone rebootsKeeper and camera start on boot; Tailscale comes back as always-on VPNNothing else needed: no PIN to typeNo
Tailscale down, or its key expiredSpacesheep control stream (thermal/p3-control, verified by token_sha256) and editor buttons still reach the camera app & keeper, including full OTA APK upgrades (./deploy-phone.sh --spacesheep)Nightly reboot reconnects itNo
Spacesheep downTailscale still reaches the keeperThe camera's LAN viewer still worksNo
Lab Wi-Fi downGoogle Fi LTE SIM: evaluateNetworkFailover() binds the process to cellular & reconnects WebRTC + OTA automaticallySwitches back to Wi-Fi (or USB-Ethernet eth0) as soon as it returnsNo
The keeper itself breaksWireless ADB (kept on across reboots via WRITE_SECURE_SETTINGS, port discovered via :8080 or TCP scan)In-lab laptop via ./deploy-phone.sh --usbRare
Charger unplugged / power cutunplugged_alert fires after 10 min on battery (hours before the battery runs flat)Rung 6 smart-plug reboot (only when battery ≥ 30%)Only if long outage
Android freezes completelyNothing in software (optional Fingerbot on power button + USB webcam view)Hold the power button 30 sYes, rare

With cellular LTE failover, WRITE_SECURE_SETTINGS wireless ADB keep-alive, and kick_tailscale, only a full kernel hang requires a physical button press.

The reset ladder: each rung runs only when the one below it didn't help

  1. Re-sync the P3 stream (or frozen-frame auto-reset)Camera app, seconds (auto at 45 s stalled or 150 identical FNV-1a frame hashes). Remote button reset_stream / ?action=soft_reset
    exists
  2. Re-handshake USB (+ wedged bulk EP reset), restart WebRTCCamera app, seconds (auto at 90 s stalled; calls resetDevice() + CLEAR_FEATURE(ENDPOINT_HALT) if bulk EP 0x81 wedged). Remote button hard_reset / ?action=hard_reset
    exists
  3. Restart the camera app processCamera app + Keeper + shell watchdog, ~8 s (auto at 180 s stalled). Remote button restart_app / ./deploy-phone.sh --restart-app
    exists
  4. Reinstall the last good buildSnapshots verified APK after 25 s at ≥10 fps; auto-rolls back if a new update fails within 10 min. Remote button rollback / ./deploy-phone.sh --rollback
    exists
  5. Reboot the phoneDevicePolicyManager / Keeper / shell svc power reboot (auto at 600 s stalled). Remote button reboot_phone / ./deploy-phone.sh --reboot
    exists
  6. Nightly reboot at 04:00 & smart-plug power-cut rebootIdle nightly reboot at 04:00 (?action=nightly_reboot&enabled=1&hour=4) + 45–360 s charger power-cut detector when battery ≥ 30% (?action=power_cycle_reboot&enabled=1)
    exists
Out of band
  1. Power-cycle the smart plug (45–360 s)You, from the plug's app; the phone detects the 45–360 s power cut on charger return and reboots itself
    detector built (plug to buy)
  2. Someone in the lab holds power 30 s (or runs ./deploy-phone.sh --usb)Roman, Nate, or you with a USB-C laptop cable
    last resort

An update reaches the lab phone only after the home phone ran it

  1. Build and signDeterministic keystore support (P3_KEYSTORE_FILE or shared debug keystore, backed up to ~/.config/spacesheep/p3-signing-backup.keystore). The version number goes up every build, and the APK carries only the 64-hex SHA-256 digest (DEFAULT_TOKEN_SHA256).
  2. PublishAPK and SHA-256 staged via ./deploy-phone.sh --http (Tailscale/LAN/Cellular), ./deploy-phone.sh --spacesheep (private GitHub release ota-latest + thermal/p3-control over Wi-Fi or Cellular SIM), or ./deploy-phone.sh --usb in the lab.
  3. Pre-upgrade snapshotBefore installing any new APK, the phone verifies free disk space (freeStorageMb), snapshots the currently running verified APK to last_good.apk, and backs up settings to com.thermal.p3keeper.
  4. Lab phoneCamera app or Keeper pulls the APK, verifies the SHA-256 digest, and installs with no tap via PackageInstaller.
  5. 10-minute verification checkOnce frames flow at ≥10 fps with pictureHealthy == true for 25 s, the build is marked currentBuildVerifiedGood = true and cached as last_good.apk. If 10 minutes pass after an update with the P3 plugged in and the build still hasn't streamed cleanly, it automatically rolls back to last_good.apk.

Today (v1.3.8, code 11 + com.thermal.p3keeper v1.0.1, code 2): all five steps, automatic 10-minute verification, cached last_good.apk rollback, Direct Boot (LOCKED_BOOT_COMPLETED), persistent USB permission, cellular SIM failover, and the separate Keeper app on port 8081 are live on the Realme 14T 5G.

What's built in v1.3.8 + Keeper v1.0.1: camera app, keeper app, cellular failover, USB lab workflow & watchdog

Keeper app (:keeper)

com.thermal.p3keeper v1.0.1 (port 8081)

  • exists Separate Android APK (directBootAware="true") with KeeperDeviceAdminReceiver
  • exists HTTP control server on port 8081 (/health, /restart-camera, /rollback-camera, /reboot, /upgrade-camera, /kick-tailscale) behind token_sha256
  • exists 30 s watchdog probing 127.0.0.1:8080/health + re-asserting adb_wifi_enabled = 1 via WRITE_SECURE_SETTINGS; auto-restarts com.thermal.p3stream after 3 misses
  • exists Reports listenPorts (from /proc/net/tcp*) so deploy-phone.sh finds the wireless ADB port even when :8080 is down
  • exists Fallback Spacesheep thermal/p3-control polling + KeeperSettingsProvider backup/restore
Camera app (:app)

All reliability & discussion items built (v1.3.8, code 11)

  • exists Unattended Direct Boot (LOCKED_BOOT_COMPLETED + persistent IUsbManager USB grant + WRITE_SECURE_SETTINGS wireless ADB keep-alive)
  • exists Automatic Wi-Fi ↔ Cellular LTE ↔ USB-Ethernet (eth0) failover via bindProcessToNetwork + WebRTC ICE reconnect
  • exists 64-bit FNV-1a lastFrameHash + pictureHealthy check (auto-resets stream after 150 identical frames; resets wedged bulk EP via resetDevice())
  • exists Tailscale VPN auto-kick (com.tailscale.ipn.CONNECT_VPN after 60 s down) + Device Owner freeze policies
  • exists Post-mortem last_crash.json + getHistoricalProcessExitReasons + freeStorageMb + wifiRssiDbm + 10-min unplugged_alert + battery_overheat
  • exists Zero raw Spacesheep key in APK (DEFAULT_TOKEN_SHA256 only) + keystore backed up to ~/.config/spacesheep/p3-signing-backup.keystore
  • exists Configurable external dead-man's switch (?action=healthcheck_url, pinged every 5 min when healthy)
At home, lab USB & remote CLI

Watchdog, 3-path drill & USB deploy

  • exists tools/p3_watchdog.py: probes :8080/health (checking picture_healthy), :8081/health, and Spacesheep telemetry; auto-kicks Tailscale; alerts on unplugged_alert and battery_overheat
  • exists python3 tools/p3_watchdog.py --drill: non-disruptive end-to-end test of all 3 control paths (:8080 ping_drill, :8081/health, and Spacesheep thermal/p3-control round-trip)
  • exists ./deploy-phone.sh --usb (fast in-lab USB-C laptop deploy), --http, --spacesheep, --reboot, --rollback, --restart-app, --status
  • exists Remote buttons on the live camera page: reset_stream, hard_reset, restart_app, rollback, reboot_phone

In-lab wired debugging & Anker USB-C hub topology

When the Anker USB-C hub (0x291a:0x8355) is plugged into the Realme 14T 5G, the phone is the USB Host (DFP, data_role=host) so it can drive the Thermal Master P3 camera (0x3474:0x45A2). Because USB 2.0 is strict single-host and the Anker hub's USB-C PD input port is power-only while its downstream ports are host-facing, plugging a laptop into a downstream port of the hub will not enumerate the phone as an ADB device. When you walk into the lab with a laptop, use any of these three fast workflows:

1. Direct USB-C cable (fastest when Wi-Fi is down)

./deploy-phone.sh --usb

  • Unplug the Anker hub from the phone and plug your laptop's USB-C cable directly into the phone (the phone immediately switches to USB Device / UFP mode and enumerates on `adb devices -l`).
  • Run ./deploy-phone.sh --usb (builds `:app` + `:keeper`, installs both over USB, re-applies `WRITE_SECURE_SETTINGS`, disables app hibernation & 7-day ADB timeout, and starts `p3_proc_helper.sh` in ~12 s).
  • Unplug the laptop and plug the Anker hub back in — the phone returns to USB Host mode and the P3 resumes 25 Hz streaming in ~3 s without waking the screen.
2. Zero-unplug wired access via the hub

USB-Ethernet dongle on the Anker hub

  • Plug a USB-A or USB-C Ethernet adapter (`eth0`) into any free downstream data port of the Anker hub (alongside the P3 camera).
  • Because the phone is the USB Host, Android brings up `eth0` (`TRANSPORT_ETHERNET`) while the P3 keeps streaming at 25 Hz.
  • In v1.3.8, `StorageUtils.evaluateNetworkFailover()` recognizes `TRANSPORT_ETHERNET` and exposes `:8080`, `:8081`, and Wireless ADB over the wired Ethernet IP (`/api/status.phone.lanIps`).
3. Zero-unplug wireless ADB + USB port forwarding

./deploy-phone.sh realme

  • Leave the Anker hub and P3 plugged in.
  • Run ./deploy-phone.sh realme — it queries `:8080/api/status.phone.listenPorts` (or Keeper `:8081/health.listenPorts`, or runs a 1-second 128-thread TCP scan of `32768–60999` if both HTTP servers are down), connects `adb connect`, and sets up `adb forward tcp:8080 tcp:8080` + `tcp:8081 tcp:8081`.

Hardware & lab spares checklist

ItemWhyStatus
Anker USB-C hub (`0x291a:0x8355`) with charging pass-throughThe P3 and the charger share the phone's one portIn use since 15 Oct: charges at 1.39 A, +28%/h while streaming; plain 5 V, no PD
Google Fi data SIM (`rmnet_data2`)Automatic second network when lab Wi-Fi drops (both live streaming & OTA updates)Active & verified 15 Oct (automatic failover in v1.3.8)
Smart plug with its own cloud appA reset that needs neither Tailscale nor Spacesheep to reach the phone (cut power 45–360 s; phone auto-reboots when power returns)Detector + 10-min `unplugged_alert` built in v1.3.8; plug to buy
Lab spares drawer (hub, OTG cable, pre-provisioned backup phone)Swap a failed hub or phone in 2 minutes; backup signing key is at `~/.config/spacesheep/p3-signing-backup.keystore`Pixel 9 Pro XL available as backup phone
A card on the mount (+ optional Fingerbot / Pi host later)"If it's dark and nothing responds: hold power 30 s"To print

Unattended boot & remote reboot instructions

  1. What already happens automatically on every reboot (even before PIN unlock): Both com.thermal.p3stream and com.thermal.p3keeper are 100% directBootAware="true" and store their config in Device-Protected storage (/data/user_de/0/...), with persistent USB permission registered in Android's IUsbManager via p3_shell_tool.jar and WRITE_SECURE_SETTINGS keeping wireless ADB on. On LOCKED_BOOT_COMPLETED (before any PIN is entered), the phone immediately starts the P3 USB driver at 25 Hz, starts port 8080 & port 8081 HTTP servers, starts publishing 2.5 fps thermal stills + 12 Hz telemetry to Spacesheep (over Wi-Fi or Google Fi LTE SIM), and starts polling thermal/p3-control. Once unlocked (or immediately on boot if Screen Lock = None), full 25 fps WebRTC video starts too.
  2. Turn off the lockscreen PIN so Tailscale also starts on reboot with zero human input: Because the third-party Tailscale app (com.tailscale.ipn) is not Direct-Boot-aware, Android blocks Tailscale from starting until the phone is unlocked (or has no screen lock). Remove the PIN once:
    • On the phone UI: Settings → Passwords & security → Lock screen password → Turn off password (enter current PIN).
    • Or via ADB: adb shell locksettings clear --old YOUR_PIN
    • Verify in /api/status that bootReadiness.lockScreenDisabled == true and bootReadiness.readyForUnattendedReboot == true.
  3. How to reboot, recover, or drill the phone remotely:
    • Non-disruptive 3-path drill: python3 tools/p3_watchdog.py --drill (verifies `:8080`, `:8081`, and `thermal/p3-control` round-trip in ~6 s without interrupting streaming).
    • CLI (Tailscale/LAN or automatic fallback to Spacesheep over Wi-Fi/LTE): ./deploy-phone.sh --reboot realme (or --restart-app, --rollback, --http, --spacesheep, --usb).
    • Live stream page buttons: Click Restart App, Rollback APK, or Reboot Phone on thermal-master-p3-live-camera-stream.
    • Direct HTTP (port 8080 or Keeper port 8081): curl -X POST -H "Authorization: Bearer $SPACESHEEP_API_KEY" "http://100.98.192.61:8080/api/control?action=reboot" (or http://100.98.192.61:8081/reboot).
    • Smart-plug power cycle: Turn off the charger smart plug for 2 minutes (any window between 45 s and 360 s), then turn it back on — RollbackAndRebootManager detects the power-cut window and triggers a full system reboot.
    • Scheduled nightly reboot: Enabled by default at 04:00 local time when no HTTP stream clients are connected and uptime > 6 h (configurable via ?action=nightly_reboot&enabled=1&hour=4).
  4. Optional Device Owner elevation (enables OS OTA 30-day freeze, `DISALLOW_SAFE_BOOT`, `DISALLOW_FACTORY_RESET`, always-on VPN lock, and `DevicePolicyManager.reboot()`): Remove accounts temporarily, run adb shell dpm set-device-owner com.thermal.p3keeper/.KeeperDeviceAdminReceiver (or com.thermal.p3stream/.P3DeviceAdminReceiver), then re-add accounts. Even without Device Owner, ./deploy-phone.sh grants WRITE_SECURE_SETTINGS, disables app hibernation and 7-day ADB expiry, and starts p3_proc_helper.sh as UID 2000 (com.android.shell, which holds android.permission.REBOOT and MANAGE_USB).

5-layer defense-in-depth & Google account isolation (v1.3.9 + Keeper v1.0.2)

Because the Spacesheep live streaming URL (thermal-master-p3-live-camera-stream) is shared publicly on the internet while a personal Google account is signed into the phone, five independent security layers isolate the camera stream from the phone and its accounts:

Defense layerWhat could an internet attacker try?How v1.3.9 + Keeper v1.0.2 block it
1. Public Spacesheep stream & telemetry lockdownRead internal LAN/Tailscale/Cellular IPs or open ports from thermal/p3-telemetry, or send button/OTA commands over /-/eventspublicViewerLock = true and encryptPublicTelemetry = true by default: plaintext IPs, ports, device model, and crash traces are stripped from public telemetry and encrypted with AES-256-GCM (priv_enc, keyed by SHA-256("p3-telemetry-priv-v1:" + apiKey)). Every mutating or privileged command on /-/events and thermal/p3-control requires constant-time verifyAuthToken().
2. Network & HTTP port firewall (:8080 & :8081)Scan or connect to port 8080/8081 over the Google Fi cellular address, or trigger cross-origin CSRF from a malicious web pageStorageUtils.isAllowedHttpPeer(socket) drops any socket arriving on the cellular interface or from a non-RFC1918 / non-Tailscale (100.64.0.0/10) IP before reading a single HTTP byte. Cross-origin browser requests are rejected with 403 Forbidden, non-loopback /api/control requires Bearer token auth by default (requireControlToken = true), unauthenticated /api/status & /health redact IPs/ports/processes, and per-IP rate limiting locks out 5 failed token checks for 5 minutes (429 Too Many Requests).
3. Cryptographic OTA & APK supply-chain lockTrick the phone into downloading an untrusted APK via upgrade_urlinstallApkFromUrl on both :8080 and Keeper :8081 enforces StorageUtils.isAllowedOtaUrl(url) (HTTPS-only + strict host allowlist: github.com, api.github.com, *.githubusercontent.com, spacesheep.dev), requires a mandatory 64-hex sha256 digest verified before session.commit(), and Android's PackageInstaller enforces signature match against ~/.config/spacesheep/p3-signing-backup.keystore.
4. Optical RGB camera privacy lock & WebView sandboxTurn on the phone's front/rear optical RGB camera or microphone to spy on the lab, or exploit WebView file accessrgbLockedOff = true by default: the phone's optical Camera2/WebRTC pipeline is disabled and can only be unlocked via token-authenticated ?action=rgb_lock&enabled=0. AlwaysActiveWebView disables file/content/universal URL access and geolocation, restricts navigation to https://spacesheep.dev, and denies PermissionRequest.RESOURCE_AUDIO_CAPTURE and RESOURCE_VIDEO_CAPTURE.
5. Personal Google account & app isolationAccess Gmail, Chrome, Google Drive, Photos, Calendar, Keep, Maps, or YouTube on the phone, or extract backed-up tokensandroid:allowBackup="false" on both APKs prevents Android Auto-Backup from uploading shared_prefs to Google Drive. KeeperSettingsProvider enforces PackageManager.SIGNATURE_MATCH so no third-party app can query backed-up settings. ./deploy-phone.sh --lockdown-google (and ?action=lockdown_google&enabled=1) force-stops and disables (pm disable-user --user 0) Gmail, Chrome, Drive, Photos, YouTube, Maps, Keep, and Calendar while keeping Play Services & Tailscale active.

Five decisions for you

Which phone goes to the lab?The Realme 14T carries the P3 and Google Fi SIM today and runs v1.3.9 + Keeper v1.0.2 with tap-free updates, Direct Boot, cellular failover, 5-layer security hardening, and self-reboot. The Pixel 9 Pro XL stays available as a spare or test phone.
Your tailnet, not the lab's?Recommended. Anyone on the lab's tailnet can read the lab boxes, and the phone's control port shouldn't be theirs.
Write the keeper, or run an MDM?The keeper (com.thermal.p3keeper v1.0.2) is now built and installed alongside the camera app, keeping the Spacesheep path with no server to run.
Does the lab Wi-Fi let it out?It needs outbound HTTPS to Tailscale and spacesheep.dev, and no sign-in page — and if lab Wi-Fi drops or blocks it, the Google Fi LTE SIM or a GL.iNet travel router / USB-Ethernet dongle carries both streaming and OTA updates.
Who in the lab presses the button?Roman or Nate, for the last rung, on the rare day it's needed.
What the app has today (v1.3.9 + Keeper v1.0.2), and what to check
  • Five control & recovery paths exist in v1.3.9 (code 12): (1) port 8080 HTTP (/api/control, POST /api/upgrade) and port 8081 Keeper HTTP (/health, /restart-camera, /rollback-camera, /reboot, /upgrade-camera, /upgrade-keeper, /kick-tailscale), authenticated via your Spacesheep token or its SHA-256 digest and firewall-restricted to loopback/LAN/Tailscale; (2) the Spacesheep editor events channel on thermal/p3-telemetry/-/events (protected by publicViewerLock + operator token SHA-256); (3) the outbound Spacesheep control stream thermal/p3-control over Wi-Fi or Google Fi LTE SIM; (4) the 45–360 s charger power-cut detector for smart-plug reboots; and (5) in-lab USB-C deployment (./deploy-phone.sh --usb).
  • All items from the 14–15 Oct reliability checklist, Discussion #1–#8, and the 5-layer security hardening architecture are built: Direct Boot (LOCKED_BOOT_COMPLETED) + Device-Protected storage, persistent IUsbManager USB permission, WRITE_SECURE_SETTINGS wireless ADB keep-alive, automatic Wi-Fi ↔ Cellular LTE ↔ USB-Ethernet failover, 64-bit FNV-1a pictureHealthy frozen-frame reset, wedged bulk EP resetDevice() recovery, kick_tailscale, 10-min unplugged_alert + battery_overheat, last_crash.json + OS exit reasons, keystore backup, configurable healthcheck_url, AES-256-GCM priv_enc telemetry encryption, personal Google app lockdown (--lockdown-google), and tools/p3_watchdog.py --drill.
  • USB host vs. device: with the Anker hub plugged in, the phone is the USB host (DFP). To debug over a USB-C cable in the lab, unplug the Anker hub and run ./deploy-phone.sh --usb (or plug a USB-Ethernet adapter into a downstream port of the Anker hub).
  • Built from the app's source in ~/git/thermal-android, the thermal camera hub (the hub, the lab's people) and the AI Foundry hub (the lab's tailnet, the Intel's watchdog).

Part of the thermal camera hub. Live stream: thermal-master-p3-live-camera-stream.