| name | otacon-cli |
| description | Control Android phones in a fleet remotely via the otacon CLI. Use for phone automation โ tapping UI elements, typing text, reading SMS, taking screenshots, accessibility tree, notifications, clipboard, apps, calls, eSIM, recording โ and fleet management โ registering this CLI client, listing/managing phones/hosts/dongles, approving registrations. Triggers on tasks involving phone automation, Android control, UI testing, mobile device management, fleet management, SIM/eSIM provisioning. |
| license | MIT |
| metadata | {"author":"otacon","version":"2.0.0"} |
Otacon CLI
Control Android phones connected to a Raspberry Pi fleet, via a central registry. The CLI talks to the registry to discover where each phone lives, then makes per-phone calls directly to the host that owns it. Everything runs over Tailscale.
Setup (one-time per machine)
otacon auth register --registry http://otacon-registry.<tailnet>.ts.net:9080
otacon reg list
otacon reg approve <pending_id>
otacon phones list
otacon phones use phone-2
After this, the token + registry URL are saved to ~/.otacon/config.toml (chmod 0600). Subsequent commands work without flags.
Env var overrides
All config values can be overridden per-invocation via env vars (precedence: env > flag > config file):
| Env var | Purpose |
|---|
OTACON_REGISTRY_URL | Registry endpoint |
OTACON_TOKEN | Bearer admin token |
OTACON_PHONE | Active phone ID (also via --phone <id>) |
OTACON_CONFIG_DIR | Config directory (default ~/.otacon) |
How to invoke
The deployed CLI binary is otacon:
otacon <subcommand> ...
All examples in this skill use otacon.
Inside this repo (development), the CLI hasn't been globally installed. Use the pnpm wrapper instead โ it runs the in-tree TypeScript source:
pnpm cli <subcommand> ...
So otacon phones list becomes pnpm cli phones list when working from inside the repo. Pick whichever matches your environment.
Output format
List/status commands default to column-aligned tables with colored status fields. Pass --json for raw JSON (for piping to jq or programmatic use).
otacon phones list
otacon phones list --json
Fleet management
Phones
otacon phones list
otacon phones list --connected
otacon phones list --host otacon-pi
otacon phones use <phone-id>
otacon phones delete <phone-id>
otacon phones factory-reset
otacon phones status [<phone-id>]
otacon phones location [<id>]
otacon config get
otacon config set bluetooth_enabled=off
SIM/eSIM (per phone)
Maps directly to the host's /api/sims/* endpoints:
otacon sims list
otacon sims install <activation-code>
otacon sims delete <sub-id>
otacon sims switch <sub-id>
otacon sims enable <sub-id>
otacon sims disable <sub-id>
otacon sims defaults
eSIM install is platform-aware:
- Pixel: uses the Settings UI flow (walks through a state machine that opens
Settings โ Add eSIM โ manual code entry โ confirm). This is necessary because
EuiccManager.downloadSubscription() requires carrier privilege that third-party
Device Owner apps don't have.
- Samsung / other: uses
EuiccManager.downloadSubscription() via the kiosk app
bridge with an auto-tap watcher for the carrier confirmation dialog.
After install, the profile is typically disabled. Enable + switch to activate:
otacon sims list
otacon sims enable <sub-id>
otacon sims switch <sub-id>
APN overrides (per phone)
Maps directly to the host's /api/apns/* endpoints. APN ids are assigned by
Android DevicePolicyManager.addOverrideApn() and are shown by list/create.
otacon apns list
otacon apns upsert SpeedTalk --operator 310240 --apn stkmobi --mmsc <mms-url>
otacon apns create SpeedTalk --operator "310 240" --apn stkmobi
otacon apns update <apn-id> --types default,mms,supl --protocol ipv4v6 --mmsc <mms-url>
otacon apns delete <apn-id>
otacon apns status
otacon apns enable
otacon apns disable
apns list shows each APN row's enabled flag. The global Android override APN
switch is separate; check it with otacon apns status.
Defaults for minimal data APNs:
types: default,supl
protocol: ipv4v6
roamingProtocol: ipv4v6
authType: none
MMS-capable APNs also support --mmsc, --mms-proxy, and --mms-port.
Create/upsert auto-adds mms to types when MMS fields are present; for
manual update, pass --types ...mms... if the existing APN is not already
MMS-capable.
Wi-Fi (per phone)
Wi-Fi is controlled directly on the active phone and persisted as host-local
Rust config. There is no user-facing wifi connect; provisioning owns network
selection.
otacon wifi status
otacon wifi on
otacon wifi off
otacon info
When wifi off is set, the fleet-agent monitor skips Wi-Fi setup and Wi-Fi
healing for that phone.
Registry config
Registry config is fleet policy pushed through the central registry. Bluetooth
stays here because it controls pairing/dongle assignment intent.
otacon config get
otacon config set bluetooth_enabled=off
otacon config set bluetooth_enabled=on
Adding a new UI variant for eSIM install (for agents):
The Pixel Settings UI flow has text that varies by Android version and phone model.
When a new variant appears, use the snapshot + manual walk-through to map it:
otacon key wake --phone <id>
otacon key home --phone <id>
otacon snapshot --phone <id>
otacon tap <ref> --phone <id>
otacon set-text <ref> '<activation-code>' --phone <id>
The state machine is in src/server/src/api/esim_ui.rs. Each state has a detection
predicate (text match on the snapshot) and an action (tap button, enter text, wait).
Add new variants by updating detection text and action targets.
Hosts (Pi nodes)
otacon hosts list
otacon hosts status <id>
otacon hosts delete <id>
Dongles (USB BT adapters)
otacon dongles list
otacon dongles delete <id>
Registrations
Both new hosts (Pi nodes) and new clients (CLIs/UIs) request registration; an admin approves them.
otacon reg list
otacon reg approve <id>
otacon reg reject <id>
otacon reg approve-all
otacon reg reject-all
Admin clients (other CLIs/UIs that share access)
otacon clients list
otacon clients list --all
otacon clients revoke <token-id>
Auth
otacon auth register --registry <url>
otacon auth unregister
otacon auth whoami
Per-phone automation (top-level commands)
These operate on the active phone (set via phones use) or --phone <id>.
Always check screen state first
Before taking a screenshot, snapshot, or interacting with UI, run otacon info
and check screen_state. Possible values:
screen_state | Meaning | Can you interact? |
|---|
unlocked | Awake, no keyguard, foreground app visible | Yes โ proceed |
locked | Awake but lock screen showing | Snapshot/screenshot work but show lock screen, not your app |
asleep | Display off, deep sleep | No โ wake first |
dozing | Ambient/AOD low-power display | No โ wake first |
dreaming | Screensaver running | No โ wake first |
unknown | Couldn't determine (ADB error) | Treat as asleep |
Also useful from info: activity (current foreground activity) and
window (focused window). When activity is empty/null, the phone is
likely asleep.
To wake a phone:
otacon key wake
otacon swipe 540 1500 540 500
Core observation loop
otacon info
otacon info --monitor
otacon screenshot -o screen.png
otacon snapshot
otacon snapshot --json
info returns: model, resolution, screen_state, current activity and
window, wifi, bt_connected, vnc_port (the host port to VNC into),
phone stats (CPU/mem/battery/temp), and phone_number. The
monitor field (verbose fleet-agent setup/health blob) is hidden by
default; pass --monitor to include it.
The accessibility tree assigns ref IDs (e0, e1, ...) to interactive elements. Refs are monotonic, stable for the same UI state, and only assigned to interactive elements. Prefer ref-based actions over raw coordinates.
UI actions
otacon tap e5
otacon tap 540 1200
otacon long-tap e5
otacon long-tap 540 1200
otacon type "hello world"
otacon set-text e3 "Hello, world!"
otacon swipe 540 1500 540 500
otacon swipe 540 1500 540 500 --duration 500
otacon pinch 540 1200 100 300
otacon pinch 540 1200 300 100
otacon scroll e7
otacon scroll e7 --up
otacon key home
otacon key back
otacon key enter
Recognized key names (or pass a raw Android keycode):
- Navigation:
home, back, recents/app_switch, menu
- Power:
power (toggle), wake/wakeup, sleep
- Volume:
volume_up, volume_down
- Editing:
enter, delete/backspace, tab, space, escape/esc
- Modifiers:
ctrl, shift, alt, meta/cmd/search
- Calls:
call, end_call/endcall
- Letters:
aโz (lowercase, single char)
- Raw: any digit string (e.g.
otacon key 24)
SMS
otacon sms list
otacon sms read <thread_id>
otacon sms send "+1234567890" "message body"
Calls
otacon call dial "+1234567890"
otacon call answer
otacon call hangup
otacon call status
Notifications
otacon notifications list
otacon notifications dismiss "<key>"
otacon notifications action "<key>" <index>
Keys often start with special characters โ use -- before the key if it starts with a dash:
otacon notifications dismiss -- "0|com.example|123|null|10045"
Clipboard
otacon clipboard get
otacon clipboard set "copied text"
Apps
otacon apps list
otacon apps running
otacon apps launch com.android.chrome
otacon apps stop com.android.chrome
otacon apps install /path/to/app.apk
otacon apps install /path/to/app.apkm
apps running returns both the apps list AND the current screen_state,
so when the list is empty you'll see e.g. (no running apps โ phone is dozing. Wake with: otacon key wake) instead of just an empty result.
Open URI / deep link
otacon open "https://example.com"
otacon open "tel:+1234567890"
otacon open "instagram://user?username=example"
Contacts
otacon contacts search "John"
Screen recording
otacon record
otacon record -d 60
otacon record -d 60 -o video.mp4
otacon record start
otacon record start -d 60
otacon record status
otacon record stop
otacon record stop -o video.mp4
Records video + audio (mp4). Default 5min, max 10min. Only one recording at a time. Auto-stops at max โ call record stop to retrieve the file.
Architecture quick reference
- Registry (
http://otacon-registry.<tailnet>.ts.net:9080) โ central index of fleet state. Mirrors host state via reliable events (outbox + reconciler โ see AGENTS.md). CLI talks here for fleet queries.
- Host (
https://otacon-pi.<tailnet>.ts.net:8080) โ Pi running phones. CLI talks here directly for per-phone actions, after looking up the address from the registry.
- Resolution flow: CLI calls
GET /admin/phones/{id} โ extracts host.address + host.api_port + maps registry phone ID to host-local ID via adb_serial โ makes direct HTTPS call to https://{address}:{port}/phones/{local_id}/api/....
Tips for AI agents
- Set an active phone first (
phones use <id>) so subsequent commands don't need --phone. Or pass OTACON_PHONE=<id> env var per invocation.
- Always pass
--json when piping to jq or other tools. Default tables include ANSI color codes that won't parse cleanly.
- Take a snapshot before acting โ refs are only valid for the current UI state. After a tap/scroll, take another snapshot.
- Prefer
set-text over type for non-ASCII text or when targeting a specific input field by ref.
- Snapshots are cached briefly โ after performing an action, the cache is invalidated. Re-snapshot to see updated state.
- Notification keys often start with special chars โ quote them and use
-- separator.
screenshot.png saved by the wrapper goes to src/cli/ (the package dir), not $PWD. Pass an absolute path with -o $PWD/screenshot.png if running from a different directory.
- Use
otacon auth whoami to debug "not registered" issues โ it shows which registry + token are actually being used after env var/config resolution.