Protocol
The wire format between your code, the PC server and the phones. One JSON object per line, so any language can drive a phone.
This page summarizes the protocol. The normative text, including the key schedule and the relay frames, is PROTOCOL.md in the repository, and the commands themselves are defined in commands.json.
Ports
| Port | Protocol | Listens on | For |
|---|---|---|---|
| 8778 | UDP | all interfaces | Phones finding the PC on Wi-Fi |
| 8779 | TCP | all interfaces | Phones: plain, TLS or WebSocket on the same port |
| 8780 | TCP | 127.0.0.1 | Your code: NDJSON, HTTP or WebSocket on the same port |
Your code to the server
UTF-8 JSON, one object per line. Each request has an id you choose and a cmd:
{"id":1,"cmd":"touch","device":"shelf-01","by":"id","value":"login"}
{"id":1,"ok":true,"via":"node","ms":412}
- Leave out
devicewhen exactly one phone is online. - Parameters are named as in the reference. Positional values also work:
{"id":2,"cmd":"tap","args":[540,1200]}. - Shorthands such as
touchByIdare accepted ascmd. - A single result value comes back under
value:{"id":3,"ok":true,"value":false}. - Errors:
{"id":4,"ok":false,"error":"NOT_FOUND","msg":"…","retryable":true,"screen":"com.example/.Main"}. - Requests can be pipelined. Replies to different phones may arrive out of order; replies for one phone keep their order.
Events
After {"id":9,"cmd":"subscribe","events":["notification","device"]} the connection also receives lines without an id:
{"event":"device","device":"k7d2q9xa","name":"shelf-01","state":"online","route":"lan"}
{"event":"notification","device":"k7d2q9xa","key":"0|com.kakao.talk|1|null|10123","package":"com.kakao.talk","title":"Kim","text":"See you at 3"}
Kinds: device, notification, screen (foreground app changed), toast, and result (the late result of a command that answered accepted).
Network-cutting commands
data(false), wifi(false), airplane(true) and batches containing them reply {"ok":true,"accepted":true} first and deliver {"event":"result","id":…} once the phone is back. Add "wait":true to get the final result as the reply instead. "offline_wait": seconds overrides how long a command waits for an offline phone.
The server to a phone
The phone connects to the PC and sends two plaintext handshake lines: its ephemeral P-256 key and a nonce, answered by the server's. Both sides then derive keys with HKDF-SHA256 from two ECDH results: ephemeral with ephemeral (forward secrecy) and static with static (both sides prove who they are). In QR pairing a one-time token is mixed in as well; in code pairing both sides show a 6-digit code derived from the same keys.
Every line after that is an envelope:
{"seq":0,"blob":"<base64url AES-256-GCM ciphertext>"}
seq counts up by one in each direction and doubles as the nonce, so a replayed or reordered line is rejected. Blobs over 512 KiB are split into parts with "more":true.
Inside, the server sends commands with its own id, and the phone numbers everything it sends with n:
{"id":17,"cmd":"touch","by":"id","value":"com.kakao.talk:id/login","nth":0,"timeout":10}
{"id":17,"n":42,"ok":true,"via":"node","ms":412}
The phone keeps messages until the server acknowledges n, and remembers the last 2000 command IDs, so a reconnect loses nothing and repeats nothing. Keepalive pings run every 25 seconds.
Discovery
{"droidline":"discover","v":1,"server":"k3j9d0a2mq"}
{"droidline":"here","v":1,"server":"k3j9d0a2mq","name":"OFFICE-PC","port":8779,"pub":"BD3x…","pairing":true}
The reply is not trusted on its own; the handshake checks the PC's key.
Test vectors
test-vectors.json holds fixed keys, handshake lines, derived keys, 6-digit codes and encrypted envelopes. The Go server, the Android app and the relay all test against it; an implementation in another language can too.