Skip to main content

Custom connection

note

Daemon connections defines how to connect to devices directly. It works on low-level ADB packets, and generally requires exclusive device accesses.

You may also interested in custom transports, which works on high-level ADB sockets, and can provide shared access to a device.

We have seen AdbDaemonWebUsbDevice class in USB connection and AdbDaemonDirectSocketsDevice class in TCP connection, but they are high-level abstractions for managing devices. In fact, AdbDaemonTransport only needs a pair of ReadableStream<AdbPacket> and WriteableStream<Consumable<AdbPacket>> to work, that's what the connect method in those Device classes returns.

note

See Web Streams Basics page for a quick introduction to ReadableStream, WriteableStream, and other types from Web Streams API.

AdbPacket

ADB protocol is based on packets. AdbDaemonTransport works on deserialized AdbPacket objects, and a connection is responsible for transferring those packets between device and AdbDaemonTransport. USB and TCP connections convert them to raw bytes and send through USB or TCP, while a custom connection may transfer them using JSON, protobuf, or any other formats over TCP, WebSocket, WebTransport, or any other transportation.

Each AdbPacket is an plain object with the following fields:

  • command: A 32-bit unsigned integer.
  • arg0: A 32-bit unsigned integer.
  • arg1: A 32-bit unsigned integer. arg0 and arg1 have different meanings for different commands.
  • payload: A Uint8Array.
  • checksum: A 32-bit unsigned integer calculated by summing all bytes in payload.
  • magic: A 32-bit unsigned integer calculated by command ^ 0xffffffff.

AdbDaemonTransport will give the connection AdbPackets with all fields filled, but the connection can omit checksum and magic fields when passing packets to AdbDaemonTransport, as those are not essential for basic operations.

Create ReadableStream

As mentioned in Web Streams Basics, it's very simple to convert pull or push style data sources into ReadableStreams.

Pull style data source

For pull style data sources, simply use the ReadableStream:

import { AdbPacketData } from "@yume-chan/adb";
import { ReadableStream } from "@yume-chan/stream-extra";

declare function readFromSource(): AdbPacketData | undefined;

const readable = new ReadableStream({
pull(controller) {
const packet: AdbPacketData | undefined = readFromSource();
if (packet) {
controller.enqueue(packet);
} else {
controller.close();
}
},
});

Push style data source

For push style data sources, you can use PushReadableStream to easily convert it to a ReadableStream:

import { AdbPacketData } from "@yume-chan/adb";
import { PushReadableStream } from "@yume-chan/stream-extra";

declare function onPacket(handler: (packet: AdbPacketData | undefined) => Promise<void>): void;

const readable = new PushReadableStream((controller) => {
onPacket(async (packet) => {
if (packet) {
await controller.enqueue(packet);
} else {
controller.close();
}
});
});

Push style data source with backpressure

Ideally, if the data source supports pause and resume, you should propagate the ReadableStream backpressure to the source. For example, with a Node.js Readable:

import { Readable } from "node:stream";
import { AdbPacketData } from "@yume-chan/adb";
import { PushReadableStream } from "@yume-chan/stream-extra";

declare const source: Readable;

const readable = new PushReadableStream((controller) => {
source.on("data", async (chunk: AdbPacketData) => {
source.pause();
await controller.enqueue(chunk);
source.resume();
});
source.on("end", () => {
controller.close();
});
});

Create WriteableStream

The write side needs to be a WritableStream of Consumable<AdbPacketInit>:

Consumable<AdbPacketInit> allows the data producer to reuse the payload buffer, which reduces memory allocations and GC overhead to improve performance. The consumer must call consume after using the value (written to a socket, copied to another buffer, etc.) to notify the producer that the value can be reused.

import { AdbPacketInit } from "@yume-chan/adb";
import { WritableStream, Consumable } from "@yume-chan/stream-extra";

declare function writeToSink(packet: AdbPacketInit | undefined): void;

const writeable = new WritableStream<Consumable<AdbPacketInit>>({
write(chunk) {
writeToSink(chunk.value);
chunk.consume();
},
end() {
writeToSink(undefined);
},
});

Create stream pair

AdbDaemonTransport requires a ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>, which is a plain object with readable and writable fields containing the above two streams:

import { AdbPacketData, AdbPacketInit } from "@yume-chan/adb";
import { ReadableWritablePair, Consumable } from "@yume-chan/stream-extra";

const connection: ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>> = {
readable,
writable,
};

Convert between raw bytes

The core package also has helper methods to convert streams between Uint8Array and AdbPacket:

import { AdbPacket, AdbPacketInit, AdbPacketData, AdbPacketSerializeStream } from "@yume-chan/adb";
import {
WrapReadableStream,
WrapWritableStream,
ReadableStream,
StructDeserializeStream,
MaybeConsumable,
} from "@yume-chan/stream-extra";

declare const rawReadable: ReadableStream<Uint8Array>;

const readable: ReadableStream<AdbPacketData> = new WrapReadableStream(rawReadable).pipeThrough(
new StructDeserializeStream(AdbPacket),
);

declare const rawWritable: WritableStream<Uint8Array>;

const writable: WritableStream<MaybeConsumable<AdbPacketInit>> = new WrapWritableStream(rawWritable)
.bePipedThroughFrom(new MaybeConsumable.UnwrapStream())
.bePipedThroughFrom(new AdbPacketSerializeStream());