Skip to content
Visor Developers

JSON-RPC

The VDK speaks JSON-RPC 2.0, so any language can use it, through two doors: Visor's adb socket, described here, and the network door, a WebSocket with the same messages.

Reaching the socket

Visor listens on the abstract Unix socket visor-kit while Developer mode is on, and lets in only adb's shell user. Forward a port to it:

adb forward tcp:7333 localabstract:visor-kit

Then connect to 127.0.0.1:7333. Remove the forward when done: adb forward --remove tcp:7333. If nothing answers and the connection closes at once, Visor is not running or its Developer mode is off. Developer mode opens only this socket: apps never need it.

The shell holds every scope, and also has the shell.* methods: shell.apps, shell.grant, shell.revoke, shell.simulate, shell.inject, and the topic shell.trace.

Framing

One JSON object per line, UTF-8, ending in \n, both ways. Requests carry an id you choose; answers carry the same id. Several requests can be in flight at once.

-> {"jsonrpc":"2.0","id":1,"method":"kit.hello","params":{"protocol":1,"kit":"my-script 1.0"}}
<- {"jsonrpc":"2.0","id":1,"result":{"protocol":1,"visor":"2.4.0","granted":["state","camera","band","display"],"methods":[...],"topics":[...]}}
-> {"jsonrpc":"2.0","id":2,"method":"glasses.status"}
<- {"jsonrpc":"2.0","id":2,"result":{"paired":true,"link":"connected","battery":82,"worn":true}}

Say kit.hello first. Params are by name, as an object; a method without params takes {} or none.

Errors

<- {"jsonrpc":"2.0","id":3,"error":{"code":-32000,"message":"The glasses are not connected.","data":{"code":"not_connected","message":"The glasses are not connected."}}}

error.data is the VDK's Failure, with code, message, and sometimes scope and reason; see Errors. error.code is -32601 for an unknown method, -32602 for params that do not fit, -32700 for a line that is not JSON-RPC, and -32000 for every other failure.

Streams

JSON-RPC has no streams, so the VDK adds three methods and two notifications:

Name Params Answer
Method kit.subscribe topic, params (optional) {"subscription": ID}, where ID is the request's own id
Method kit.unsubscribe subscription {}
Method kit.cancel id of a running call {}
Notification kit.event subscription, event
Notification kit.ended subscription, failure (absent when the stream simply ended)
-> {"jsonrpc":"2.0","id":4,"method":"kit.subscribe","params":{"topic":"band.gestures"}}
<- {"jsonrpc":"2.0","id":4,"result":{"subscription":4}}
<- {"jsonrpc":"2.0","method":"kit.event","params":{"subscription":4,"event":{"finger":"index","action":"tap","derived":"tap","time":1793111111111}}}

The answer to kit.subscribe always comes before the subscription's first event.

Feeds

A feed, such as camera.live, streams binary records. Over JSON-RPC it is subscribed with kit.subscribe like a topic, and each event is a record: its header, and its payload in Base64.

-> {"jsonrpc":"2.0","id":5,"method":"kit.subscribe","params":{"topic":"camera.live","params":{"quality":"low"}}}
<- {"jsonrpc":"2.0","id":5,"result":{"subscription":5}}
<- {"jsonrpc":"2.0","method":"kit.event","params":{"subscription":5,"event":{"header":{"kind":"video","codec":"video/hevc","ptsUs":0,"config":true,"width":360,"height":640},"payload":"AAAAAUAB..."}}}

A keyframe in Base64 runs to hundreds of kilobytes at full_hd: allow lines that long. The Python and TypeScript clients have feed, which decodes the payload.

The contract

protocol/api/openrpc.json is an OpenRPC document of every method, its params and its answer, with every type under components.schemas. The VDK's own additions:

  • x-scope: the scope a method or topic needs.
  • x-since: the protocol version that added it.
  • x-shell: answered only to the adb shell.
  • x-topics: the topics, each with its params and its event.
  • x-feeds: the feeds, each with its params and its record's header.

Generate a client from it, or read it as the reference. It is written by the build from the Kotlin contract, so it is always exact.

A client in Python

import json, socket, subprocess

subprocess.run(["adb", "forward", "tcp:7333", "localabstract:visor-kit"], check=True)
sock = socket.create_connection(("127.0.0.1", 7333))
lines = sock.makefile("r", encoding="utf-8")

def call(id, method, params=None):
    sock.sendall((json.dumps({"jsonrpc": "2.0", "id": id, "method": method, "params": params or {}}) + "\n").encode())
    while True:
        message = json.loads(lines.readline())
        if message.get("id") == id:
            return message

call(1, "kit.hello", {"protocol": 1, "kit": "python-example"})
print(call(2, "glasses.status")["result"])
call(3, "kit.subscribe", {"topic": "band.gestures"})
for line in lines:
    message = json.loads(line)
    if message.get("method") == "kit.event":
        print(message["params"]["event"])