mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
feat(aoa): emulating HID keyboard via AOA protocol
This commit is contained in:
parent
258547c50b
commit
d08a6891e7
28 changed files with 2118 additions and 881 deletions
|
@ -22,6 +22,7 @@
|
|||
"@yume-chan/adb-backend-ws": "workspace:^0.0.9",
|
||||
"@yume-chan/adb-credential-web": "workspace:^0.0.18",
|
||||
"@yume-chan/android-bin": "workspace:^0.0.18",
|
||||
"@yume-chan/aoa": "workspace:^0.0.18",
|
||||
"@yume-chan/async": "^2.2.0",
|
||||
"@yume-chan/b-tree": "workspace:^0.0.16",
|
||||
"@yume-chan/event": "workspace:^0.0.18",
|
||||
|
@ -46,6 +47,7 @@
|
|||
"@mdx-js/loader": "^2.2.1",
|
||||
"@mdx-js/react": "^2.2.1",
|
||||
"@next/mdx": "^13.1.1",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/react": "18.0.27",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-next": "13.1.5",
|
||||
|
|
224
apps/demo/src/components/scrcpy/input.ts
Normal file
224
apps/demo/src/components/scrcpy/input.ts
Normal file
|
@ -0,0 +1,224 @@
|
|||
import { AoaHidDevice, HidKeyCode, HidKeyboard } from "@yume-chan/aoa";
|
||||
import { Disposable } from "@yume-chan/event";
|
||||
import {
|
||||
AdbScrcpyClient,
|
||||
AndroidKeyCode,
|
||||
AndroidKeyEventAction,
|
||||
AndroidKeyEventMeta,
|
||||
} from "@yume-chan/scrcpy";
|
||||
|
||||
export interface KeyboardInjector extends Disposable {
|
||||
down(key: string): Promise<void>;
|
||||
up(key: string): Promise<void>;
|
||||
reset(): Promise<void>;
|
||||
}
|
||||
|
||||
export class ScrcpyKeyboardInjector implements KeyboardInjector {
|
||||
private readonly client: AdbScrcpyClient;
|
||||
|
||||
private _controlLeft = false;
|
||||
private _controlRight = false;
|
||||
private _shiftLeft = false;
|
||||
private _shiftRight = false;
|
||||
private _altLeft = false;
|
||||
private _altRight = false;
|
||||
private _metaLeft = false;
|
||||
private _metaRight = false;
|
||||
|
||||
private _capsLock = false;
|
||||
private _numLock = true;
|
||||
|
||||
private _keys: Set<AndroidKeyCode> = new Set();
|
||||
|
||||
public constructor(client: AdbScrcpyClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
private setModifier(keyCode: AndroidKeyCode, value: boolean) {
|
||||
switch (keyCode) {
|
||||
case AndroidKeyCode.ControlLeft:
|
||||
this._controlLeft = value;
|
||||
break;
|
||||
case AndroidKeyCode.ControlRight:
|
||||
this._controlRight = value;
|
||||
break;
|
||||
case AndroidKeyCode.ShiftLeft:
|
||||
this._shiftLeft = value;
|
||||
break;
|
||||
case AndroidKeyCode.ShiftRight:
|
||||
this._shiftRight = value;
|
||||
break;
|
||||
case AndroidKeyCode.AltLeft:
|
||||
this._altLeft = value;
|
||||
break;
|
||||
case AndroidKeyCode.AltRight:
|
||||
this._altRight = value;
|
||||
break;
|
||||
case AndroidKeyCode.MetaLeft:
|
||||
this._metaLeft = value;
|
||||
break;
|
||||
case AndroidKeyCode.MetaRight:
|
||||
this._metaRight = value;
|
||||
break;
|
||||
case AndroidKeyCode.CapsLock:
|
||||
if (value) {
|
||||
this._capsLock = !this._capsLock;
|
||||
}
|
||||
break;
|
||||
case AndroidKeyCode.NumLock:
|
||||
if (value) {
|
||||
this._numLock = !this._numLock;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private getMetaState(): AndroidKeyEventMeta {
|
||||
let metaState = 0;
|
||||
if (this._altLeft) {
|
||||
metaState |=
|
||||
AndroidKeyEventMeta.AltOn | AndroidKeyEventMeta.AltLeftOn;
|
||||
}
|
||||
if (this._altRight) {
|
||||
metaState |=
|
||||
AndroidKeyEventMeta.AltOn | AndroidKeyEventMeta.AltRightOn;
|
||||
}
|
||||
if (this._shiftLeft) {
|
||||
metaState |=
|
||||
AndroidKeyEventMeta.ShiftOn | AndroidKeyEventMeta.ShiftLeftOn;
|
||||
}
|
||||
if (this._shiftRight) {
|
||||
metaState |=
|
||||
AndroidKeyEventMeta.ShiftOn | AndroidKeyEventMeta.ShiftRightOn;
|
||||
}
|
||||
if (this._controlLeft) {
|
||||
metaState |=
|
||||
AndroidKeyEventMeta.CtrlOn | AndroidKeyEventMeta.CtrlLeftOn;
|
||||
}
|
||||
if (this._controlRight) {
|
||||
metaState |=
|
||||
AndroidKeyEventMeta.CtrlOn | AndroidKeyEventMeta.CtrlRightOn;
|
||||
}
|
||||
if (this._metaLeft) {
|
||||
metaState |=
|
||||
AndroidKeyEventMeta.MetaOn | AndroidKeyEventMeta.MetaLeftOn;
|
||||
}
|
||||
if (this._metaRight) {
|
||||
metaState |=
|
||||
AndroidKeyEventMeta.MetaOn | AndroidKeyEventMeta.MetaRightOn;
|
||||
}
|
||||
if (this._capsLock) {
|
||||
metaState |= AndroidKeyEventMeta.CapsLockOn;
|
||||
}
|
||||
if (this._numLock) {
|
||||
metaState |= AndroidKeyEventMeta.NumLockOn;
|
||||
}
|
||||
return metaState;
|
||||
}
|
||||
|
||||
public async down(key: string): Promise<void> {
|
||||
const keyCode = AndroidKeyCode[key as keyof typeof AndroidKeyCode];
|
||||
if (!keyCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setModifier(keyCode, true);
|
||||
this._keys.add(keyCode);
|
||||
await this.client.controlMessageSerializer?.injectKeyCode({
|
||||
action: AndroidKeyEventAction.Down,
|
||||
keyCode,
|
||||
metaState: this.getMetaState(),
|
||||
repeat: 0,
|
||||
});
|
||||
}
|
||||
|
||||
public async up(key: string): Promise<void> {
|
||||
const keyCode = AndroidKeyCode[key as keyof typeof AndroidKeyCode];
|
||||
if (!keyCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setModifier(keyCode, false);
|
||||
this._keys.delete(keyCode);
|
||||
await this.client.controlMessageSerializer?.injectKeyCode({
|
||||
action: AndroidKeyEventAction.Up,
|
||||
keyCode,
|
||||
metaState: this.getMetaState(),
|
||||
repeat: 0,
|
||||
});
|
||||
}
|
||||
|
||||
public async reset(): Promise<void> {
|
||||
this._controlLeft = false;
|
||||
this._controlRight = false;
|
||||
this._shiftLeft = false;
|
||||
this._shiftRight = false;
|
||||
this._altLeft = false;
|
||||
this._altRight = false;
|
||||
this._metaLeft = false;
|
||||
this._metaRight = false;
|
||||
for (const key of this._keys) {
|
||||
this.up(AndroidKeyCode[key]);
|
||||
}
|
||||
this._keys.clear();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
export class AoaKeyboardInjector implements KeyboardInjector {
|
||||
public static async register(
|
||||
device: USBDevice
|
||||
): Promise<AoaKeyboardInjector> {
|
||||
const keyboard = await AoaHidDevice.register(
|
||||
device,
|
||||
0,
|
||||
HidKeyboard.DESCRIPTOR
|
||||
);
|
||||
return new AoaKeyboardInjector(keyboard);
|
||||
}
|
||||
|
||||
private readonly aoaKeyboard: AoaHidDevice;
|
||||
private readonly hidKeyboard = new HidKeyboard();
|
||||
|
||||
public constructor(aoaKeyboard: AoaHidDevice) {
|
||||
this.aoaKeyboard = aoaKeyboard;
|
||||
}
|
||||
|
||||
public async down(key: string): Promise<void> {
|
||||
const keyCode = HidKeyCode[key as keyof typeof HidKeyCode];
|
||||
if (!keyCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hidKeyboard.down(keyCode);
|
||||
await this.aoaKeyboard.sendInputReport(
|
||||
this.hidKeyboard.serializeInputReport()
|
||||
);
|
||||
}
|
||||
|
||||
public async up(key: string): Promise<void> {
|
||||
const keyCode = HidKeyCode[key as keyof typeof HidKeyCode];
|
||||
if (!keyCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hidKeyboard.up(keyCode);
|
||||
await this.aoaKeyboard.sendInputReport(
|
||||
this.hidKeyboard.serializeInputReport()
|
||||
);
|
||||
}
|
||||
|
||||
public async reset(): Promise<void> {
|
||||
this.hidKeyboard.reset();
|
||||
await this.aoaKeyboard.sendInputReport(
|
||||
this.hidKeyboard.serializeInputReport()
|
||||
);
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
await this.aoaKeyboard.unregister();
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { ADB_SYNC_MAX_PACKET_SIZE } from "@yume-chan/adb";
|
||||
import { AdbWebUsbBackend } from "@yume-chan/adb-backend-webusb";
|
||||
import {
|
||||
AdbScrcpyClient,
|
||||
AdbScrcpyOptions1_22,
|
||||
AndroidKeyCode,
|
||||
AndroidScreenPowerMode,
|
||||
CodecOptions,
|
||||
DEFAULT_SERVER_PATH,
|
||||
|
@ -25,6 +25,11 @@ import { action, autorun, makeAutoObservable, runInAction } from "mobx";
|
|||
import { GLOBAL_STATE } from "../../state";
|
||||
import { ProgressStream } from "../../utils";
|
||||
import { fetchServer } from "./fetch-server";
|
||||
import {
|
||||
AoaKeyboardInjector,
|
||||
KeyboardInjector,
|
||||
ScrcpyKeyboardInjector,
|
||||
} from "./input";
|
||||
import { MuxerStream, RECORD_STATE } from "./recorder";
|
||||
import { H264Decoder, SETTING_STATE } from "./settings";
|
||||
|
||||
|
@ -58,7 +63,7 @@ export class ScrcpyPageState {
|
|||
|
||||
client: AdbScrcpyClient | undefined = undefined;
|
||||
hoverHelper: ScrcpyHoverHelper | undefined = undefined;
|
||||
pressedKeys: Set<AndroidKeyCode> = new Set();
|
||||
keyboard: KeyboardInjector | undefined = undefined;
|
||||
|
||||
async pushServer() {
|
||||
const serverBuffer = await fetchServer();
|
||||
|
@ -351,6 +356,14 @@ export class ScrcpyPageState {
|
|||
this.hoverHelper = new ScrcpyHoverHelper();
|
||||
this.running = true;
|
||||
});
|
||||
|
||||
if (GLOBAL_STATE.backend instanceof AdbWebUsbBackend) {
|
||||
this.keyboard = await AoaKeyboardInjector.register(
|
||||
GLOBAL_STATE.backend.device
|
||||
);
|
||||
} else {
|
||||
this.keyboard = new ScrcpyKeyboardInjector(client);
|
||||
}
|
||||
} catch (e: any) {
|
||||
GLOBAL_STATE.showErrorDialog(e);
|
||||
} finally {
|
||||
|
@ -376,7 +389,8 @@ export class ScrcpyPageState {
|
|||
RECORD_STATE.recording = false;
|
||||
}
|
||||
|
||||
this.pressedKeys.clear();
|
||||
this.keyboard?.dispose();
|
||||
this.keyboard = undefined;
|
||||
|
||||
this.fps = "0";
|
||||
clearTimeout(this.fpsCounterIntervalId);
|
||||
|
|
|
@ -111,7 +111,6 @@ function handlePointerLeave(e: PointerEvent<HTMLDivElement>) {
|
|||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Because pointer capture on pointer down, this event only happens for hovering mouse and pen.
|
||||
// Release the injected pointer, otherwise it will stuck at the last position.
|
||||
injectTouch(AndroidMotionEventAction.HoverExit, e);
|
||||
|
|
79
apps/demo/src/pages/audio.tsx
Normal file
79
apps/demo/src/pages/audio.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { DefaultButton, PrimaryButton } from "@fluentui/react";
|
||||
import { AdbWebUsbBackend } from "@yume-chan/adb-backend-webusb";
|
||||
import {
|
||||
aoaGetProtocol,
|
||||
aoaSetAudioMode,
|
||||
aoaStartAccessory,
|
||||
} from "@yume-chan/aoa";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useCallback, useState } from "react";
|
||||
import { GLOBAL_STATE } from "../state";
|
||||
|
||||
function AudioPage() {
|
||||
const [supported, setSupported] = useState<boolean | undefined>(undefined);
|
||||
const handleQuerySupportClick = useCallback(async () => {
|
||||
const backend = GLOBAL_STATE.backend as AdbWebUsbBackend;
|
||||
const device = backend.device;
|
||||
const version = await aoaGetProtocol(device);
|
||||
setSupported(version >= 2);
|
||||
}, []);
|
||||
|
||||
const handleEnableClick = useCallback(async () => {
|
||||
const backend = GLOBAL_STATE.backend as AdbWebUsbBackend;
|
||||
const device = backend.device;
|
||||
const version = await aoaGetProtocol(device);
|
||||
if (version < 2) {
|
||||
return;
|
||||
}
|
||||
await aoaSetAudioMode(device, 1);
|
||||
await aoaStartAccessory(device);
|
||||
}, []);
|
||||
const handleDisableClick = useCallback(async () => {
|
||||
const backend = GLOBAL_STATE.backend as AdbWebUsbBackend;
|
||||
const device = backend.device;
|
||||
const version = await aoaGetProtocol(device);
|
||||
if (version < 2) {
|
||||
return;
|
||||
}
|
||||
await aoaSetAudioMode(device, 0);
|
||||
await aoaStartAccessory(device);
|
||||
}, []);
|
||||
|
||||
if (
|
||||
!GLOBAL_STATE.backend ||
|
||||
!(GLOBAL_STATE.backend instanceof AdbWebUsbBackend)
|
||||
) {
|
||||
return <div>Audio forward can only be used with WebUSB backend.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
Supported:{" "}
|
||||
{supported === undefined ? "Unknown" : supported ? "Yes" : "No"}
|
||||
</div>
|
||||
<div>
|
||||
<PrimaryButton
|
||||
disabled={!GLOBAL_STATE.backend}
|
||||
onClick={handleQuerySupportClick}
|
||||
>
|
||||
Query Support
|
||||
</PrimaryButton>
|
||||
<DefaultButton
|
||||
disabled={!supported}
|
||||
onClick={handleEnableClick}
|
||||
>
|
||||
Enable
|
||||
</DefaultButton>
|
||||
<DefaultButton
|
||||
disabled={!supported}
|
||||
onClick={handleDisableClick}
|
||||
>
|
||||
Disable
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(AudioPage);
|
|
@ -1,11 +1,6 @@
|
|||
import { Dialog, LayerHost, ProgressIndicator, Stack } from "@fluentui/react";
|
||||
import { useId } from "@fluentui/react-hooks";
|
||||
import { makeStyles, shorthands } from "@griffel/react";
|
||||
import {
|
||||
AndroidKeyCode,
|
||||
AndroidKeyEventAction,
|
||||
AndroidKeyEventMeta,
|
||||
} from "@yume-chan/scrcpy";
|
||||
import { WebCodecsDecoder } from "@yume-chan/scrcpy-decoder-webcodecs";
|
||||
import { action, runInAction } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
@ -132,29 +127,7 @@ async function handleKeyEvent(e: KeyboardEvent<HTMLDivElement>) {
|
|||
e.stopPropagation();
|
||||
|
||||
const { type, code } = e;
|
||||
const keyCode = AndroidKeyCode[code as keyof typeof AndroidKeyCode];
|
||||
if (keyCode) {
|
||||
if (type === "keydown") {
|
||||
STATE.pressedKeys.add(keyCode);
|
||||
} else {
|
||||
STATE.pressedKeys.delete(keyCode);
|
||||
}
|
||||
|
||||
// TODO: workaround the missing keyup event on macOS https://crbug.com/1393524
|
||||
STATE.client!.controlMessageSerializer!.injectKeyCode({
|
||||
action:
|
||||
type === "keydown"
|
||||
? AndroidKeyEventAction.Down
|
||||
: AndroidKeyEventAction.Up,
|
||||
keyCode,
|
||||
metaState:
|
||||
(e.ctrlKey ? AndroidKeyEventMeta.CtrlOn : 0) |
|
||||
(e.shiftKey ? AndroidKeyEventMeta.ShiftOn : 0) |
|
||||
(e.altKey ? AndroidKeyEventMeta.AltOn : 0) |
|
||||
(e.metaKey ? AndroidKeyEventMeta.MetaOn : 0),
|
||||
repeat: 0,
|
||||
});
|
||||
}
|
||||
STATE.keyboard;
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
|
@ -162,18 +135,7 @@ function handleBlur() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Release all pressed keys on window blur,
|
||||
// Because there will not be any keyup events when window is not focused.
|
||||
for (const key of STATE.pressedKeys) {
|
||||
STATE.client.controlMessageSerializer!.injectKeyCode({
|
||||
action: AndroidKeyEventAction.Up,
|
||||
keyCode: key,
|
||||
metaState: 0,
|
||||
repeat: 0,
|
||||
});
|
||||
}
|
||||
|
||||
STATE.pressedKeys.clear();
|
||||
STATE.keyboard?.reset();
|
||||
}
|
||||
|
||||
const Scrcpy: NextPage = () => {
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
PersonFeedbackRegular,
|
||||
PhoneLaptopRegular,
|
||||
PhoneRegular,
|
||||
PhoneSpeakerRegular,
|
||||
PlayRegular,
|
||||
PlugConnectedRegular,
|
||||
PlugDisconnectedRegular,
|
||||
|
@ -86,6 +87,7 @@ export function register() {
|
|||
PersonFeedback: <PersonFeedbackRegular style={STYLE} />,
|
||||
Phone: <PhoneRegular style={STYLE} />,
|
||||
PhoneLaptop: <PhoneLaptopRegular style={STYLE} />,
|
||||
PhoneSpeaker: <PhoneSpeakerRegular style={STYLE} />,
|
||||
Play: <PlayRegular style={STYLE} />,
|
||||
PlugConnected: <PlugConnectedRegular style={STYLE} />,
|
||||
PlugDisconnected: <PlugDisconnectedRegular style={STYLE} />,
|
||||
|
@ -155,6 +157,7 @@ const Icons = {
|
|||
PersonFeedback: "PersonFeedback",
|
||||
Phone: "Phone",
|
||||
PhoneLaptop: "PhoneLaptop",
|
||||
PhoneSpeaker: "PhoneSpeaker",
|
||||
Play: "Play",
|
||||
PlugConnected: "PlugConnected",
|
||||
PlugDisconnected: "PlugDisconnected",
|
||||
|
|
1685
common/config/rush/pnpm-lock.yaml
generated
1685
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||
{
|
||||
"pnpmShrinkwrapHash": "3e5a716ba1e8bfb6c9b5bd7018741744d14f80d0",
|
||||
"pnpmShrinkwrapHash": "8b04c1bdf0fa11f10bb4abc856fd9d1c0903908e",
|
||||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
||||
}
|
||||
|
|
11
libraries/aoa/.eslintrc.cjs
Normal file
11
libraries/aoa/.eslintrc.cjs
Normal file
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
"extends": [
|
||||
"@yume-chan"
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: [
|
||||
"./tsconfig.test.json"
|
||||
],
|
||||
},
|
||||
}
|
16
libraries/aoa/.npmignore
Normal file
16
libraries/aoa/.npmignore
Normal file
|
@ -0,0 +1,16 @@
|
|||
.rush
|
||||
|
||||
# Test
|
||||
coverage
|
||||
**/*.spec.ts
|
||||
**/*.spec.js
|
||||
**/*.spec.js.map
|
||||
**/__helpers__
|
||||
jest.config.js
|
||||
|
||||
.eslintrc.cjs
|
||||
tsconfig.json
|
||||
tsconfig.test.json
|
||||
|
||||
# Logs
|
||||
*.log
|
21
libraries/aoa/LICENSE
Normal file
21
libraries/aoa/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 Simon Chan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
libraries/aoa/README.md
Normal file
3
libraries/aoa/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @yume-chan/aoa
|
||||
|
||||
A TypeScript implementation of [Android Open Accessory](https://source.android.com/docs/core/interaction/accessories/protocol) protocol using WebUSB API.
|
42
libraries/aoa/package.json
Normal file
42
libraries/aoa/package.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "@yume-chan/aoa",
|
||||
"version": "0.0.18",
|
||||
"description": "TypeScript implementation of Android Open Accessory protocol.",
|
||||
"keywords": [
|
||||
"adb",
|
||||
"android-phone"
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Simon Chan",
|
||||
"email": "cnsimonchan@live.com",
|
||||
"url": "https://chensi.moe/blog"
|
||||
},
|
||||
"homepage": "https://github.com/yume-chan/ya-webadb/tree/main/packages/aoa#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yume-chan/ya-webadb.git",
|
||||
"directory": "packages/aoa"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/yume-chan/ya-webadb/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "esm/index.js",
|
||||
"types": "esm/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -b tsconfig.build.json",
|
||||
"build:watch": "tsc -b tsconfig.build.json",
|
||||
"lint": "eslint src/**/*.ts --fix",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/w3c-web-usb": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@yume-chan/eslint-config": "workspace:^1.0.0",
|
||||
"@yume-chan/tsconfig": "workspace:^1.0.0",
|
||||
"eslint": "^8.31.0",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
125
libraries/aoa/src/audio.ts
Normal file
125
libraries/aoa/src/audio.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import { AoaRequestType } from "./type.js";
|
||||
|
||||
// The original plan is to add more audio modes,
|
||||
// but AOA audio accessory mode is soon deprecated in Android 8.
|
||||
export enum AoaAudioMode {
|
||||
Off,
|
||||
/**
|
||||
* 2 channel, 16 bit, 44.1KHz PCM
|
||||
*/
|
||||
On,
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio mode. This method must be called before {@link aoaStartAccessory}.
|
||||
*
|
||||
* AOA audio accessory mode turns the Android device into a USB microphone,
|
||||
* all system audio will be directed to the microphone, to be capture by the USB host.
|
||||
*
|
||||
* It's like connecting a audio cable between the Android headphone jack and PC microphone jack,
|
||||
* except all signals are digital.
|
||||
*
|
||||
* Audio mode is deprecated in Android 8. On Android 9 and later, this call still switches the device
|
||||
* to audio accessory mode, and the device will be recognized as a USB microphone, but the
|
||||
* required USB endpoint is not presented anymore.
|
||||
* @param device The Android device.
|
||||
* @param mode The audio mode.
|
||||
*/
|
||||
export async function aoaSetAudioMode(device: USBDevice, mode: AoaAudioMode) {
|
||||
await device.controlTransferOut(
|
||||
{
|
||||
recipient: "device",
|
||||
requestType: "vendor",
|
||||
request: AoaRequestType.SetAudioMode,
|
||||
value: mode,
|
||||
index: 0,
|
||||
},
|
||||
new ArrayBuffer(0)
|
||||
);
|
||||
}
|
||||
|
||||
function findAudioStreamingInterface(device: USBDevice) {
|
||||
for (const configuration of device.configurations) {
|
||||
for (const interface_ of configuration.interfaces) {
|
||||
for (const alternate of interface_.alternates) {
|
||||
// Audio
|
||||
if (alternate.interfaceClass !== 0x01) {
|
||||
continue;
|
||||
}
|
||||
// AudioStreaming
|
||||
if (alternate.interfaceSubclass !== 0x02) {
|
||||
continue;
|
||||
}
|
||||
if (alternate.endpoints.length === 0) {
|
||||
continue;
|
||||
}
|
||||
return { configuration, interface_, alternate };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No matched alternate interface found");
|
||||
}
|
||||
|
||||
/**
|
||||
* It doesn't work on Web, because Chrome blocked audio devices from WebUSB API.
|
||||
* @param device The Android device.
|
||||
* @returns A readable stream of raw audio data.
|
||||
*/
|
||||
export function aoaGetAudioStream(device: USBDevice) {
|
||||
let endpointNumber!: number;
|
||||
return new ReadableStream<Uint8Array>({
|
||||
async start() {
|
||||
const { configuration, interface_, alternate } =
|
||||
findAudioStreamingInterface(device);
|
||||
|
||||
if (
|
||||
device.configuration?.configurationValue !==
|
||||
configuration.configurationValue
|
||||
) {
|
||||
await device.selectConfiguration(
|
||||
configuration.configurationValue
|
||||
);
|
||||
}
|
||||
|
||||
if (!interface_.claimed) {
|
||||
await device.claimInterface(interface_.interfaceNumber);
|
||||
}
|
||||
|
||||
if (
|
||||
interface_.alternate.alternateSetting !==
|
||||
alternate.alternateSetting
|
||||
) {
|
||||
await device.selectAlternateInterface(
|
||||
interface_.interfaceNumber,
|
||||
alternate.alternateSetting
|
||||
);
|
||||
}
|
||||
|
||||
const endpoint = alternate.endpoints.find(
|
||||
(endpoint) =>
|
||||
endpoint.type === "isochronous" &&
|
||||
endpoint.direction === "in"
|
||||
);
|
||||
if (!endpoint) {
|
||||
throw new Error("No matched endpoint found");
|
||||
}
|
||||
|
||||
endpointNumber = endpoint.endpointNumber;
|
||||
},
|
||||
async pull(controller) {
|
||||
const result = await device.isochronousTransferIn(endpointNumber, [
|
||||
1024,
|
||||
]);
|
||||
for (const packet of result.packets) {
|
||||
const data = packet.data!;
|
||||
const array = new Uint8Array(
|
||||
data.buffer,
|
||||
data.byteOffset,
|
||||
data.byteLength
|
||||
);
|
||||
controller.enqueue(array);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
12
libraries/aoa/src/filter.ts
Normal file
12
libraries/aoa/src/filter.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export const AOA_DEFAULT_DEVICE_FILTERS = [
|
||||
{
|
||||
vendorId: 0x18d1,
|
||||
// accessory
|
||||
productId: 0x2d00,
|
||||
},
|
||||
{
|
||||
vendorId: 0x18d1,
|
||||
// accessory + adb
|
||||
productId: 0x2d01,
|
||||
},
|
||||
] as const satisfies readonly USBDeviceFilter[];
|
105
libraries/aoa/src/hid.ts
Normal file
105
libraries/aoa/src/hid.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { AoaRequestType } from "./type.js";
|
||||
|
||||
export async function aoaHidRegister(
|
||||
device: USBDevice,
|
||||
accessoryId: number,
|
||||
reportDescriptorSize: number
|
||||
) {
|
||||
await device.controlTransferOut(
|
||||
{
|
||||
recipient: "device",
|
||||
requestType: "vendor",
|
||||
request: AoaRequestType.RegisterHid,
|
||||
value: accessoryId,
|
||||
index: reportDescriptorSize,
|
||||
},
|
||||
new ArrayBuffer(0)
|
||||
);
|
||||
}
|
||||
|
||||
export async function aoaHidSetReportDescriptor(
|
||||
device: USBDevice,
|
||||
accessoryId: number,
|
||||
reportDescriptor: Uint8Array
|
||||
) {
|
||||
await device.controlTransferOut(
|
||||
{
|
||||
recipient: "device",
|
||||
requestType: "vendor",
|
||||
request: AoaRequestType.SetHidReportDescriptor,
|
||||
value: accessoryId,
|
||||
index: 0,
|
||||
},
|
||||
reportDescriptor
|
||||
);
|
||||
}
|
||||
|
||||
export async function aoaHidUnregister(device: USBDevice, accessoryId: number) {
|
||||
await device.controlTransferOut(
|
||||
{
|
||||
recipient: "device",
|
||||
requestType: "vendor",
|
||||
request: AoaRequestType.UnregisterHid,
|
||||
value: accessoryId,
|
||||
index: 0,
|
||||
},
|
||||
new ArrayBuffer(0)
|
||||
);
|
||||
}
|
||||
|
||||
export async function aoaHidSendInputReport(
|
||||
device: USBDevice,
|
||||
accessoryId: number,
|
||||
event: Uint8Array
|
||||
) {
|
||||
await device.controlTransferOut(
|
||||
{
|
||||
recipient: "device",
|
||||
requestType: "vendor",
|
||||
request: AoaRequestType.SendHidEvent,
|
||||
value: accessoryId,
|
||||
index: 0,
|
||||
},
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulate a HID device over AOA protocol.
|
||||
*
|
||||
* It can only send input reports, but not send feature reports nor receive output reports.
|
||||
*/
|
||||
export class AoaHidDevice {
|
||||
/**
|
||||
* Register a HID device.
|
||||
* @param device The Android device.
|
||||
* @param accessoryId An arbitrary number to uniquely identify the HID device.
|
||||
* @param reportDescriptor The HID report descriptor.
|
||||
* @returns An instance of AoaHidDevice to send events.
|
||||
*/
|
||||
public static async register(
|
||||
device: USBDevice,
|
||||
accessoryId: number,
|
||||
reportDescriptor: Uint8Array
|
||||
) {
|
||||
await aoaHidRegister(device, accessoryId, reportDescriptor.length);
|
||||
await aoaHidSetReportDescriptor(device, accessoryId, reportDescriptor);
|
||||
return new AoaHidDevice(device, accessoryId);
|
||||
}
|
||||
|
||||
private _device: USBDevice;
|
||||
private _accessoryId: number;
|
||||
|
||||
private constructor(device: USBDevice, accessoryId: number) {
|
||||
this._device = device;
|
||||
this._accessoryId = accessoryId;
|
||||
}
|
||||
|
||||
public async sendInputReport(event: Uint8Array) {
|
||||
await aoaHidSendInputReport(this._device, this._accessoryId, event);
|
||||
}
|
||||
|
||||
public async unregister() {
|
||||
await aoaHidUnregister(this._device, this._accessoryId);
|
||||
}
|
||||
}
|
7
libraries/aoa/src/index.ts
Normal file
7
libraries/aoa/src/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export * from "./audio.js";
|
||||
export * from "./filter.js";
|
||||
export * from "./hid.js";
|
||||
export * from "./initialize.js";
|
||||
export * from "./keyboard.js";
|
||||
export * from "./mouse.js";
|
||||
export * from "./type.js";
|
33
libraries/aoa/src/initialize.ts
Normal file
33
libraries/aoa/src/initialize.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { AoaRequestType } from "./type.js";
|
||||
|
||||
export async function aoaGetProtocol(device: USBDevice) {
|
||||
const result = await device.controlTransferIn(
|
||||
{
|
||||
recipient: "device",
|
||||
requestType: "vendor",
|
||||
request: AoaRequestType.GetProtocol,
|
||||
value: 0,
|
||||
index: 0,
|
||||
},
|
||||
2
|
||||
);
|
||||
const version = result.data!.getUint16(0, true);
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* The device will reset (disconnect) after this call.
|
||||
* @param device The Android device.
|
||||
*/
|
||||
export async function aoaStartAccessory(device: USBDevice) {
|
||||
await device.controlTransferOut(
|
||||
{
|
||||
recipient: "device",
|
||||
requestType: "vendor",
|
||||
request: AoaRequestType.Start,
|
||||
value: 0,
|
||||
index: 0,
|
||||
},
|
||||
new ArrayBuffer(0)
|
||||
);
|
||||
}
|
311
libraries/aoa/src/keyboard.ts
Normal file
311
libraries/aoa/src/keyboard.ts
Normal file
|
@ -0,0 +1,311 @@
|
|||
// cspell:ignore Oper
|
||||
|
||||
// Most names follow Web API `KeyboardEvent.code`,
|
||||
export enum HidKeyCode {
|
||||
KeyA = 4,
|
||||
KeyB,
|
||||
KeyC,
|
||||
KeyD,
|
||||
KeyE,
|
||||
KeyF,
|
||||
KeyG,
|
||||
KeyH,
|
||||
KeyI,
|
||||
KeyJ,
|
||||
KeyK,
|
||||
KeyL,
|
||||
KeyM,
|
||||
KeyN,
|
||||
KeyO,
|
||||
KeyP,
|
||||
KeyQ,
|
||||
KeyR,
|
||||
KeyS,
|
||||
KeyT,
|
||||
KeyU,
|
||||
KeyV,
|
||||
KeyW,
|
||||
KeyX,
|
||||
KeyY,
|
||||
KeyZ,
|
||||
Digit1,
|
||||
Digit2,
|
||||
Digit3,
|
||||
Digit4,
|
||||
Digit5,
|
||||
Digit6,
|
||||
Digit7,
|
||||
Digit8,
|
||||
Digit9,
|
||||
Digit0,
|
||||
Enter,
|
||||
Escape,
|
||||
Backspace,
|
||||
Tab,
|
||||
Space,
|
||||
Minus,
|
||||
Equal,
|
||||
BracketLeft,
|
||||
BracketRight,
|
||||
Backslash,
|
||||
NonUsHash,
|
||||
Semicolon,
|
||||
Quote,
|
||||
Backquote,
|
||||
Comma,
|
||||
Period,
|
||||
Slash,
|
||||
CapsLock,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
PrintScreen,
|
||||
ScrollLock,
|
||||
Pause,
|
||||
Insert,
|
||||
Home,
|
||||
PageUp,
|
||||
Delete,
|
||||
End,
|
||||
PageDown,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
NumLock,
|
||||
NumpadDivide,
|
||||
NumpadMultiply,
|
||||
NumpadSubtract,
|
||||
NumpadAdd,
|
||||
NumpadEnter,
|
||||
Numpad1,
|
||||
Numpad2,
|
||||
Numpad3,
|
||||
Numpad4,
|
||||
Numpad5,
|
||||
Numpad6,
|
||||
Numpad7,
|
||||
Numpad8,
|
||||
Numpad9,
|
||||
Numpad0,
|
||||
NumpadDecimal,
|
||||
NonUsBackslash,
|
||||
ContextMenu,
|
||||
Power,
|
||||
NumpadEqual,
|
||||
F13,
|
||||
F14,
|
||||
F15,
|
||||
F16,
|
||||
F17,
|
||||
F18,
|
||||
F19,
|
||||
F20,
|
||||
F21,
|
||||
F22,
|
||||
F23,
|
||||
F24,
|
||||
|
||||
Execute,
|
||||
Help,
|
||||
Menu,
|
||||
Select,
|
||||
Stop,
|
||||
Again,
|
||||
Undo,
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
Find,
|
||||
Mute,
|
||||
VolumeUp,
|
||||
VolumeDown,
|
||||
LockingCapsLock,
|
||||
LockingNumLock,
|
||||
LockingScrollLock,
|
||||
NumpadComma,
|
||||
KeypadEqualSign,
|
||||
International1,
|
||||
International2,
|
||||
International3,
|
||||
International4,
|
||||
International5,
|
||||
International6,
|
||||
International7,
|
||||
International8,
|
||||
International9,
|
||||
Lang1,
|
||||
Lang2,
|
||||
Lang3,
|
||||
Lang4,
|
||||
Lang5,
|
||||
Lang6,
|
||||
Lang7,
|
||||
Lang8,
|
||||
Lang9,
|
||||
AlternateErase,
|
||||
SysReq,
|
||||
Cancel,
|
||||
Clear,
|
||||
Prior,
|
||||
Return2,
|
||||
Separator,
|
||||
Out,
|
||||
Oper,
|
||||
ClearAgain,
|
||||
CrSel,
|
||||
ExSel,
|
||||
|
||||
Keypad00 = 0xb0,
|
||||
Keypad000,
|
||||
ThousandsSeparator,
|
||||
DecimalSeparator,
|
||||
CurrencyUnit,
|
||||
CurrencySubUnit,
|
||||
KeypadLeftParen,
|
||||
KeypadRightParen,
|
||||
KeypadLeftBrace,
|
||||
KeypadRightBrace,
|
||||
KeypadTab,
|
||||
KeypadBackspace,
|
||||
KeypadA,
|
||||
KeypadB,
|
||||
KeypadC,
|
||||
KeypadD,
|
||||
KeypadE,
|
||||
KeypadF,
|
||||
KeypadXor,
|
||||
KeypadPower,
|
||||
KeypadPercent,
|
||||
KeypadLess,
|
||||
KeypadGreater,
|
||||
KeypadAmpersand,
|
||||
KeypadDblAmpersand,
|
||||
KeypadVerticalBar,
|
||||
KeypadDblVerticalBar,
|
||||
KeypadColon,
|
||||
KeypadHash,
|
||||
KeypadSpace,
|
||||
KeypadAt,
|
||||
KeypadExclamation,
|
||||
KeypadMemStore,
|
||||
KeypadMemRecall,
|
||||
KeypadMemClear,
|
||||
KeypadMemAdd,
|
||||
KeypadMemSubtract,
|
||||
KeypadMemMultiply,
|
||||
KeypadMemDivide,
|
||||
KeypadPlusMinus,
|
||||
KeypadClear,
|
||||
KeypadClearEntry,
|
||||
KeypadBinary,
|
||||
KeypadOctal,
|
||||
KeypadDecimal,
|
||||
KeypadHexadecimal,
|
||||
|
||||
ControlLeft = 0xe0,
|
||||
ShiftLeft,
|
||||
AltLeft,
|
||||
MetaLeft,
|
||||
ControlRight,
|
||||
ShiftRight,
|
||||
AltRight,
|
||||
MetaRight,
|
||||
}
|
||||
|
||||
export class HidKeyboard {
|
||||
/**
|
||||
* A HID Keyboard Report Descriptor.
|
||||
*
|
||||
* It's compatible with the legacy boot protocol. (1 byte modifier, 1 byte reserved, 6 bytes key codes).
|
||||
* Technically it doesn't need to be compatible with the legacy boot protocol, but it's the most common implementation.
|
||||
*/
|
||||
public static readonly DESCRIPTOR = new Uint8Array(
|
||||
// prettier-ignore
|
||||
[
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x06, // Usage (Keyboard)
|
||||
0xa1, 0x01, // Collection (Application)
|
||||
0x05, 0x07, // Usage Page (Keyboard)
|
||||
0x19, 0xe0, // Usage Minimum (Keyboard Left Control)
|
||||
0x29, 0xe7, // Usage Maximum (Keyboard Right GUI)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x08, // Report Count (8)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x01, // Input (Constant)
|
||||
|
||||
0x05, 0x08, // Usage Page (LEDs)
|
||||
0x19, 0x01, // Usage Minimum (Num Lock)
|
||||
0x29, 0x05, // Usage Maximum (Kana)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x05, // Report Count (5)
|
||||
0x91, 0x02, // Output (Data, Variable, Absolute)
|
||||
|
||||
0x75, 0x03, // Report Size (3)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x91, 0x01, // Output (Constant)
|
||||
|
||||
0x05, 0x07, // Usage Page (Keyboard)
|
||||
0x19, 0x00, // Usage Minimum (Reserved (no event indicated))
|
||||
0x29, 0xdd, // Usage Maximum (Keyboard Application)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0xdd, // Logical Maximum (221)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x06, // Report Count (6)
|
||||
0x81, 0x00, // Input (Data, Array)
|
||||
0xc0 // End Collection
|
||||
]
|
||||
);
|
||||
|
||||
private _modifiers = 0;
|
||||
private _keys: Set<HidKeyCode> = new Set();
|
||||
|
||||
public down(key: HidKeyCode) {
|
||||
if (key >= HidKeyCode.ControlLeft && key <= HidKeyCode.MetaRight) {
|
||||
this._modifiers |= 1 << (key - HidKeyCode.ControlLeft);
|
||||
} else {
|
||||
this._keys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
public up(key: HidKeyCode) {
|
||||
if (key >= HidKeyCode.ControlLeft && key <= HidKeyCode.MetaRight) {
|
||||
this._modifiers &= ~(1 << (key - HidKeyCode.ControlLeft));
|
||||
} else {
|
||||
this._keys.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this._modifiers = 0;
|
||||
this._keys.clear();
|
||||
}
|
||||
|
||||
public serializeInputReport() {
|
||||
const buffer = new Uint8Array(8);
|
||||
buffer[0] = this._modifiers;
|
||||
let i = 2;
|
||||
for (const key of this._keys) {
|
||||
buffer[i] = key;
|
||||
i += 1;
|
||||
if (i >= 8) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
60
libraries/aoa/src/mouse.ts
Normal file
60
libraries/aoa/src/mouse.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
export class HidMouse {
|
||||
public static readonly descriptor = new Uint8Array(
|
||||
// prettier-ignore
|
||||
[
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x02, // Usage (Mouse)
|
||||
0xa1, 0x01, // Collection (Application)
|
||||
0x09, 0x01, // Usage (Pointer)
|
||||
0xa1, 0x00, // Collection (Physical)
|
||||
0x05, 0x09, // Usage Page (Button)
|
||||
0x19, 0x01, // Usage Minimum (Button 1)
|
||||
0x29, 0x05, // Usage Maximum (Button 5)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x05, // Report Count (5)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
|
||||
0x75, 0x03, // Report Size (3)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x01, // Input (Constant)
|
||||
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x30, // Usage (X)
|
||||
0x09, 0x31, // Usage (Y)
|
||||
0x09, 0x38, // Usage (Wheel)
|
||||
0x15, 0x81, // Logical Minimum (-127)
|
||||
0x25, 0x7f, // Logical Maximum (127)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x03, // Report Count (3)
|
||||
0x81, 0x06, // Input (Data, Variable, Relative)
|
||||
|
||||
0x05, 0x0C, // Usage Page (Consumer)
|
||||
0x0A, 0x38, 0x02, // Usage (AC Pan)
|
||||
0x15, 0x81, // Logical Minimum (-127)
|
||||
0x25, 0x7f, // Logical Maximum (127)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x06, // Input (Data, Variable, Relative)
|
||||
0xc0, // End Collection
|
||||
0xc0, // End Collection
|
||||
]
|
||||
);
|
||||
|
||||
public static serializeInputReport(
|
||||
movementX: number,
|
||||
movementY: number,
|
||||
buttons: number,
|
||||
scrollX: number,
|
||||
scrollY: number
|
||||
): Uint8Array {
|
||||
return new Uint8Array([
|
||||
buttons,
|
||||
movementX,
|
||||
movementY,
|
||||
scrollY,
|
||||
scrollX,
|
||||
]);
|
||||
}
|
||||
}
|
132
libraries/aoa/src/touchscreen.ts
Normal file
132
libraries/aoa/src/touchscreen.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
const FINGER_DESCRIPTOR = new Uint8Array(
|
||||
// prettier-ignore
|
||||
[
|
||||
0x09, 0x22, // Usage (Finger)
|
||||
0xa1, 0x02, // Collection (Logical)
|
||||
0x09, 0x51, // Usage (Contact Identifier)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x7f, // Logical Maximum (127)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
|
||||
0x09, 0x42, // Usage (Tip Switch)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
|
||||
0x95, 0x07, // Report Count (7)
|
||||
0x81, 0x03, // Input (Constant)
|
||||
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x30, // Usage (X)
|
||||
0x09, 0x31, // Usage (Y)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xff, 0x7f, // Logical Maximum (32767)
|
||||
0x35, 0x00, // Physical Minimum (0)
|
||||
0x46, 0xff, 0x7f, // Physical Maximum (32767)
|
||||
0x66, 0x00, 0x00, // Unit (None)
|
||||
0x75, 0x10, // Report Size (16)
|
||||
0x95, 0x02, // Report Count (2)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
0xc0, // End Collection
|
||||
]
|
||||
);
|
||||
|
||||
const DESCRIPTOR_HEAD = new Uint8Array(
|
||||
// prettier-ignore
|
||||
[
|
||||
0x05, 0x0d, // Usage Page (Digitizers)
|
||||
0x09, 0x04, // Usage (Touch Screen)
|
||||
0xa1, 0x01, // Collection (Application)
|
||||
0x09, 0x55, // Usage (Contact Count Maximum)
|
||||
0x25, 0x0a, // Logical Maximum (10)
|
||||
0xB1, 0x02, // Feature (Data, Variable, Absolute)
|
||||
|
||||
0x09, 0x54, // Usage (Contact Count)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x0a, // Logical Maximum (10)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
]
|
||||
);
|
||||
|
||||
const DESCRIPTOR_TAIL = new Uint8Array([
|
||||
0xc0, // End Collection
|
||||
]);
|
||||
|
||||
const DESCRIPTOR = new Uint8Array(
|
||||
DESCRIPTOR_HEAD.length +
|
||||
FINGER_DESCRIPTOR.length * 10 +
|
||||
DESCRIPTOR_TAIL.length
|
||||
);
|
||||
let offset = 0;
|
||||
DESCRIPTOR.set(DESCRIPTOR_HEAD, offset);
|
||||
offset += DESCRIPTOR_HEAD.length;
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
DESCRIPTOR.set(FINGER_DESCRIPTOR, offset);
|
||||
offset += FINGER_DESCRIPTOR.length;
|
||||
}
|
||||
DESCRIPTOR.set(DESCRIPTOR_TAIL, offset);
|
||||
|
||||
interface Finger {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A ten-point touch screen.
|
||||
*/
|
||||
export class HidTouchScreen {
|
||||
public static readonly FINGER_DESCRIPTOR = FINGER_DESCRIPTOR;
|
||||
|
||||
public static readonly DESCRIPTOR = DESCRIPTOR;
|
||||
|
||||
private fingers: Map<number, Finger> = new Map();
|
||||
|
||||
public down(id: number, x: number, y: number) {
|
||||
if (this.fingers.size >= 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fingers.set(id, {
|
||||
x,
|
||||
y,
|
||||
});
|
||||
}
|
||||
|
||||
public move(id: number, x: number, y: number) {
|
||||
const finger = this.fingers.get(id);
|
||||
if (finger) {
|
||||
finger.x = x;
|
||||
finger.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
public up(id: number) {
|
||||
this.fingers.delete(id);
|
||||
}
|
||||
|
||||
public serializeInputReport(): Uint8Array {
|
||||
const report = new Uint8Array(1 + 6 * 10);
|
||||
report[0] = this.fingers.size;
|
||||
let offset = 1;
|
||||
for (const [id, finger] of this.fingers) {
|
||||
report[offset] = id;
|
||||
offset += 1;
|
||||
|
||||
report[offset] = 1;
|
||||
offset += 1;
|
||||
|
||||
report[offset] = finger.x & 0xff;
|
||||
report[offset + 1] = (finger.x >> 8) & 0xff;
|
||||
report[offset + 2] = finger.y & 0xff;
|
||||
report[offset + 3] = (finger.y >> 8) & 0xff;
|
||||
offset += 4;
|
||||
}
|
||||
return report;
|
||||
}
|
||||
}
|
10
libraries/aoa/src/type.ts
Normal file
10
libraries/aoa/src/type.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export enum AoaRequestType {
|
||||
GetProtocol = 51,
|
||||
SendString,
|
||||
Start,
|
||||
RegisterHid,
|
||||
UnregisterHid,
|
||||
SetHidReportDescriptor,
|
||||
SendHidEvent,
|
||||
SetAudioMode,
|
||||
}
|
12
libraries/aoa/tsconfig.build.json
Normal file
12
libraries/aoa/tsconfig.build.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"types": [
|
||||
"w3c-web-usb",
|
||||
]
|
||||
}
|
||||
}
|
10
libraries/aoa/tsconfig.json
Normal file
10
libraries/aoa/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.test.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
]
|
||||
}
|
9
libraries/aoa/tsconfig.test.json
Normal file
9
libraries/aoa/tsconfig.test.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"w3c-web-usb",
|
||||
],
|
||||
},
|
||||
"exclude": []
|
||||
}
|
|
@ -7,15 +7,27 @@ export enum AndroidKeyEventAction {
|
|||
Up = 1,
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/KeyEvent.java;l=993;drc=95c1165bb895dd844e1793460710f7163dd330a3
|
||||
export enum AndroidKeyEventMeta {
|
||||
AltOn = 0x02,
|
||||
CtrlOn = 0x1000,
|
||||
AltLeftOn = 0x10,
|
||||
AltRightOn = 0x20,
|
||||
ShiftOn = 0x01,
|
||||
ShiftLeftOn = 0x40,
|
||||
ShiftRightOn = 0x80,
|
||||
CtrlOn = 0x1000,
|
||||
CtrlLeftOn = 0x2000,
|
||||
CtrlRightOn = 0x4000,
|
||||
MetaOn = 0x10000,
|
||||
MetaLeftOn = 0x20000,
|
||||
MetaRightOn = 0x40000,
|
||||
CapsLockOn = 0x100000,
|
||||
NumLockOn = 0x200000,
|
||||
ScrollLockOn = 0x400000,
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/KeyEvent.java;l=97;drc=95c1165bb895dd844e1793460710f7163dd330a3
|
||||
// Most name follows Web API `KeyboardEvent.code`,
|
||||
// Most names follow Web API `KeyboardEvent.code`,
|
||||
// Android-only (not exist in HID keyboard standard) keys are prefixed by `Android`.
|
||||
export enum AndroidKeyCode {
|
||||
AndroidHome = 3,
|
||||
|
@ -127,7 +139,7 @@ export enum AndroidKeyCode {
|
|||
AndroidFocus,
|
||||
|
||||
Plus, // Name not verified
|
||||
Menu, // Name not verified
|
||||
ContextMenu,
|
||||
AndroidNotification,
|
||||
AndroidSearch,
|
||||
|
||||
|
|
|
@ -383,6 +383,10 @@
|
|||
"shouldPublish": true,
|
||||
"versionPolicyName": "adb"
|
||||
},
|
||||
{
|
||||
"packageName": "@yume-chan/aoa",
|
||||
"projectFolder": "libraries/aoa"
|
||||
},
|
||||
{
|
||||
"packageName": "@yume-chan/b-tree",
|
||||
"projectFolder": "libraries/b-tree"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue