backend.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import { AdbPacketHeader, AdbPacketSerializeStream, type AdbBackend, type AdbPacketData, type AdbPacketInit } from '@yume-chan/adb';
  2. import { DuplexStreamFactory, pipeFrom, ReadableStream, WritableStream, type ReadableWritablePair } from '@yume-chan/stream-extra';
  3. import { EMPTY_UINT8_ARRAY, StructDeserializeStream } from '@yume-chan/struct';
  4. export const ADB_DEVICE_FILTER: USBDeviceFilter = {
  5. classCode: 0xFF,
  6. subclassCode: 0x42,
  7. protocolCode: 1,
  8. };
  9. class Uint8ArrayStructDeserializeStream implements StructDeserializeStream {
  10. private buffer: Uint8Array;
  11. private offset: number;
  12. public constructor(buffer: Uint8Array) {
  13. this.buffer = buffer;
  14. this.offset = 0;
  15. }
  16. public read(length: number): Uint8Array {
  17. const result = this.buffer.subarray(this.offset, this.offset + length);
  18. this.offset += length;
  19. return result;
  20. }
  21. }
  22. export class AdbWebUsbBackendStream implements ReadableWritablePair<AdbPacketData, AdbPacketInit>{
  23. private _readable: ReadableStream<AdbPacketData>;
  24. public get readable() { return this._readable; }
  25. private _writable: WritableStream<AdbPacketInit>;
  26. public get writable() { return this._writable; }
  27. public constructor(device: USBDevice, inEndpoint: USBEndpoint, outEndpoint: USBEndpoint) {
  28. const factory = new DuplexStreamFactory<AdbPacketData, Uint8Array>({
  29. close: async () => {
  30. try { await device.close(); } catch { /* device may have already disconnected */ }
  31. },
  32. dispose: async () => {
  33. navigator.usb.removeEventListener('disconnect', handleUsbDisconnect);
  34. },
  35. });
  36. function handleUsbDisconnect(e: USBConnectionEvent) {
  37. if (e.device === device) {
  38. factory.dispose();
  39. }
  40. }
  41. navigator.usb.addEventListener('disconnect', handleUsbDisconnect);
  42. this._readable = factory.wrapReadable(new ReadableStream<AdbPacketData>({
  43. async pull(controller) {
  44. // The `length` argument in `transferIn` must not be smaller than what the device sent,
  45. // otherwise it will return `babble` status without any data.
  46. // Here we read exactly 24 bytes (packet header) followed by exactly `payloadLength`.
  47. const result = await device.transferIn(inEndpoint.endpointNumber, 24);
  48. // TODO: webusb: handle `babble` by discarding the data and receive again
  49. // TODO: webusb: on Windows, `transferIn` throws an NetworkError when device disconnected, check with other OSs.
  50. // From spec, the `result.data` always covers the whole `buffer`.
  51. const buffer = new Uint8Array(result.data!.buffer);
  52. const stream = new Uint8ArrayStructDeserializeStream(buffer);
  53. // Add `payload` field to its type, because we will assign `payload` in next step.
  54. const packet = AdbPacketHeader.deserialize(stream) as AdbPacketHeader & { payload: Uint8Array; };
  55. if (packet.payloadLength !== 0) {
  56. const result = await device.transferIn(inEndpoint.endpointNumber, packet.payloadLength);
  57. packet.payload = new Uint8Array(result.data!.buffer);
  58. } else {
  59. packet.payload = EMPTY_UINT8_ARRAY;
  60. }
  61. controller.enqueue(packet);
  62. },
  63. }));
  64. this._writable = pipeFrom(
  65. factory.createWritable(new WritableStream({
  66. write: async (chunk) => {
  67. await device.transferOut(outEndpoint.endpointNumber, chunk);
  68. },
  69. }, {
  70. highWaterMark: 16 * 1024,
  71. size(chunk) { return chunk.byteLength; },
  72. })),
  73. new AdbPacketSerializeStream()
  74. );
  75. }
  76. }
  77. export class AdbWebUsbBackend implements AdbBackend {
  78. public static isSupported(): boolean {
  79. return !!globalThis.navigator?.usb;
  80. }
  81. public static async getDevices(): Promise<AdbWebUsbBackend[]> {
  82. var devices = await window.navigator.usb.getDevices();
  83. devices = devices.filter(device => device.manufacturerName == "MaixPy3" );
  84. console.log(devices);
  85. return devices.map(device => new AdbWebUsbBackend(device));
  86. }
  87. public static async requestDevice(): Promise<AdbWebUsbBackend | undefined> {
  88. try {
  89. var device = await navigator.usb.requestDevice({ filters: [ADB_DEVICE_FILTER] });
  90. console.log(device);
  91. return new AdbWebUsbBackend(device);
  92. } catch (e) {
  93. // User cancelled the device picker
  94. if (e instanceof DOMException && e.name === 'NotFoundError') {
  95. return undefined;
  96. }
  97. throw e;
  98. }
  99. }
  100. private _device: USBDevice;
  101. public get serial(): string { return this._device.serialNumber!; }
  102. public get name(): string { return this._device.productName!; }
  103. public constructor(device: USBDevice) {
  104. this._device = device;
  105. }
  106. public async connect() {
  107. if (!this._device.opened) {
  108. await this._device.open();
  109. }
  110. for (const configuration of this._device.configurations) {
  111. for (const interface_ of configuration.interfaces) {
  112. for (const alternate of interface_.alternates) {
  113. if (alternate.interfaceSubclass === ADB_DEVICE_FILTER.subclassCode &&
  114. alternate.interfaceClass === ADB_DEVICE_FILTER.classCode &&
  115. alternate.interfaceSubclass === ADB_DEVICE_FILTER.subclassCode) {
  116. if (this._device.configuration?.configurationValue !== configuration.configurationValue) {
  117. // Note: Switching configuration is not supported on Windows,
  118. // but Android devices should always expose ADB function at the first (default) configuration.
  119. await this._device.selectConfiguration(configuration.configurationValue);
  120. }
  121. if (!interface_.claimed) {
  122. await this._device.claimInterface(interface_.interfaceNumber);
  123. }
  124. if (interface_.alternate.alternateSetting !== alternate.alternateSetting) {
  125. await this._device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
  126. }
  127. let inEndpoint: USBEndpoint | undefined;
  128. let outEndpoint: USBEndpoint | undefined;
  129. for (const endpoint of alternate.endpoints) {
  130. switch (endpoint.direction) {
  131. case 'in':
  132. inEndpoint = endpoint;
  133. if (outEndpoint) {
  134. return new AdbWebUsbBackendStream(this._device, inEndpoint, outEndpoint);
  135. }
  136. break;
  137. case 'out':
  138. outEndpoint = endpoint;
  139. if (inEndpoint) {
  140. return new AdbWebUsbBackendStream(this._device, inEndpoint, outEndpoint);
  141. }
  142. break;
  143. }
  144. }
  145. }
  146. }
  147. }
  148. }
  149. throw new Error('Can not find ADB interface');
  150. }
  151. }