Custom connection
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.
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
andarg1
have different meanings for differentcommand
s. - 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 AdbPacket
s 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 ReadableStream
s.
Pull style data source
For pull style data sources, simply use the ReadableStream
:
- JavaScript
- TypeScript
import { ReadableStream } from "@yume-chan/stream-extra";
const readable = new ReadableStream({
pull(controller) {
const packet = readFromSource();
if (packet) {
controller.enqueue(packet);
} else {
controller.close();
}
},
});
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
:
- JavaScript
- TypeScript
import { PushReadableStream } from "@yume-chan/stream-extra";
const readable = new PushReadableStream((controller) => {
onPacket(async (packet) => {
if (packet) {
await controller.enqueue(packet);
} else {
controller.close();
}
});
});
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
:
- JavaScript
- TypeScript
import { PushReadableStream } from "@yume-chan/stream-extra";
const readable = new PushReadableStream((controller) => {
source.on("data", async (chunk) => {
source.pause();
await controller.enqueue(chunk);
source.resume();
});
source.on("end", () => {
controller.close();
});
});
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.
- JavaScript
- TypeScript
import { WritableStream } from "@yume-chan/stream-extra";
const writeable = new WritableStream({
write(chunk) {
writeToSink(chunk.value);
chunk.consume();
},
end() {
writeToSink(undefined);
},
});
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:
- JavaScript
- TypeScript
const connection = {
readable,
writable,
};
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
:
- JavaScript
- TypeScript
import { AdbPacket, AdbPacketSerializeStream } from "@yume-chan/adb";
import {
WrapReadableStream,
WrapWritableStream,
StructDeserializeStream,
MaybeConsumable,
} from "@yume-chan/stream-extra";
const readable = new WrapReadableStream(rawReadable).pipeThrough(
new StructDeserializeStream(AdbPacket),
);
const writable = new WrapWritableStream(rawWritable)
.bePipedThroughFrom(new MaybeConsumable.UnwrapStream())
.bePipedThroughFrom(new AdbPacketSerializeStream());
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());