create branch dev and commit code

This commit is contained in:
thanawat saiyota 2026-06-09 10:50:59 +07:00
parent 3b70cc9fe8
commit ea68fa5cc4
44 changed files with 12421 additions and 214 deletions

View file

@ -14,9 +14,53 @@ import { handleAdbPayload } from '../handlers/adbPayloadHandler';
import { adbWriter } from '../stores/adbWriter';
import { WritableStream } from '@yume-chan/stream-extra';
import { env } from '$env/dynamic/public';
import type Dice_2 from '@lucide/svelte/icons/dice-2';
import { get } from 'svelte/store';
let syncConnection: any = null;
let syncOperation: Promise<unknown> = Promise.resolve();
let recipeMenuAdbConnectPromise: Promise<Adb | undefined> | null = null;
let recipeMenuAndroidServerConnectPromise: Promise<void> | null = null;
let recipeMenuAndroidServerRetryTimer: ReturnType<typeof setTimeout> | null = null;
async function runSyncOperation<T>(operation: () => Promise<T>) {
const run = syncOperation.then(operation, operation);
syncOperation = run.catch(() => {});
return await run;
}
function clearRecipeMenuAndroidServerRetry() {
if (recipeMenuAndroidServerRetryTimer) {
clearTimeout(recipeMenuAndroidServerRetryTimer);
recipeMenuAndroidServerRetryTimer = null;
}
}
function scheduleRecipeMenuAndroidServerReconnect(delayMs = 2000) {
if (recipeMenuAndroidServerRetryTimer || !getAdbInstance()) return;
recipeMenuAndroidServerRetryTimer = setTimeout(() => {
recipeMenuAndroidServerRetryTimer = null;
void connectToAndroidRecipeMenuServer(false);
}, delayMs);
}
async function connectRecipeMenuWebUsbDevice(
device: AdbDaemonWebUsbDevice,
credentialStore: AdbWebCredentialStore
) {
const connection = await device.connect();
const transport = await AdbDaemonTransport.authenticate({
connection: connection,
serial: device.serial,
credentialStore: credentialStore
});
const adb = new Adb(transport);
await saveAdbInstance(adb);
await connectToAndroidRecipeMenuServer();
return adb;
}
function isRecoverableError(error: any): boolean {
if (!error) return false;
@ -91,7 +135,7 @@ async function connectWithRetry<T>(
);
}
export async function connnectViaWebUSB() {
export async function connnectViaWebUSB(connectAndroidServer = true) {
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
console.log('usb ok', globalThis.navigator.usb);
if (device) {
@ -109,7 +153,9 @@ export async function connnectViaWebUSB() {
const adb = new Adb(transport);
await saveAdbInstance(adb);
await connectToAndroidServer();
if (connectAndroidServer) {
await connectToAndroidServer();
}
// save device info
await deviceCredentialManager.saveDeviceInfo(device);
@ -129,7 +175,8 @@ export async function connnectViaWebUSB() {
export async function connectDeviceByCred(
device: AdbDaemonWebUsbDevice,
credStore: AdbWebCredentialStore
credStore: AdbWebCredentialStore,
connectAndroidServer = true
) {
try {
const connection = await device.connect();
@ -142,7 +189,9 @@ export async function connectDeviceByCred(
const adb = new Adb(transport);
await saveAdbInstance(adb);
await connectToAndroidServer();
if (connectAndroidServer) {
await connectToAndroidServer();
}
return true;
} catch (error) {
@ -159,6 +208,112 @@ export function getAdbInstance() {
return AdbInstance.instance;
}
export async function connectRecipeMenuViaWebUSB() {
const currentInstance = getAdbInstance();
if (currentInstance) {
await connectToAndroidRecipeMenuServer();
return currentInstance;
}
if (recipeMenuAdbConnectPromise) return await recipeMenuAdbConnectPromise;
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
console.log('recipe menu usb ok', 'usb' in globalThis.navigator);
if (device) {
console.log('recipe menu connect ', device.name);
try {
const credentialStore = new AdbWebCredentialStore();
recipeMenuAdbConnectPromise = connectRecipeMenuWebUsbDevice(device, credentialStore);
const adb = await recipeMenuAdbConnectPromise;
await deviceCredentialManager.saveDeviceInfo(device);
return adb;
} catch (e: any) {
console.error('recipe menu connect error', e);
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
addNotification(
'ERR:Device is already in use by another program, please close the program and try again'
);
}
throw e;
} finally {
recipeMenuAdbConnectPromise = null;
}
}
}
export async function connectRecipeMenuDeviceByCred(
device: AdbDaemonWebUsbDevice,
credStore: AdbWebCredentialStore
) {
const currentInstance = getAdbInstance();
if (currentInstance) {
await connectToAndroidRecipeMenuServer();
return true;
}
if (recipeMenuAdbConnectPromise) {
await recipeMenuAdbConnectPromise;
return Boolean(getAdbInstance());
}
try {
recipeMenuAdbConnectPromise = connectRecipeMenuWebUsbDevice(device, credStore);
await recipeMenuAdbConnectPromise;
return true;
} catch (error) {
throw error;
} finally {
recipeMenuAdbConnectPromise = null;
}
}
export async function reconnectAndroidRecipeMenuServer() {
await connectToAndroidRecipeMenuServer(true);
}
export async function sendRecipeMenuFileToAndroid(recipe: any) {
return await sendRecipeMenuMessageToAndroid({
type: 'save_recipe_menu_file',
payload: {
time: new Date().toLocaleTimeString(),
data: recipe
}
});
}
export async function sendRecipeMenuMessageToAndroid(message: any) {
let writer: any = get(adbWriter);
if (!writer) {
if (getAdbInstance()) {
await connectToAndroidRecipeMenuServer(false);
} else {
await connectRecipeMenuViaWebUSB();
}
writer = get(adbWriter);
}
if (!writer) {
addNotification('ERR:No active Android recipe connection');
return false;
}
try {
const encoder = new TextEncoder();
await writer.write(encoder.encode(JSON.stringify(message) + '\n'));
console.log('recipe menu sent! ', JSON.stringify(message).length);
return true;
} catch (error) {
console.error('recipe menu write failed', error);
addNotification(`ERR:Failed to send recipe menu\n${error}`);
return false;
}
}
export async function executeCmd(command: string) {
let instance = getAdbInstance();
@ -232,64 +387,117 @@ export async function cleanupSync() {
}
export async function pull(filename: string, timeoutMs: number = 5000) {
let instance = getAdbInstance();
return await runSyncOperation(async () => {
let instance = getAdbInstance();
await cleanupSync();
try {
if (instance) {
let chunkList: Uint8Array<ArrayBufferLike>[] = [];
const syncProm = instance.sync();
const timeoutProm = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('sync timeout')), timeoutMs);
});
syncConnection = await Promise.race([syncProm, timeoutProm]);
const content = syncConnection.read(filename);
let result_string = '';
for await (const chunk of content) {
result_string += new TextDecoder().decode(chunk);
}
return result_string;
}
} catch (pull_error: any) {
console.log('pulling error', pull_error);
} finally {
await cleanupSync();
}
try {
if (instance) {
let chunkList: Uint8Array<ArrayBufferLike>[] = [];
const syncProm = instance.sync();
const timeoutProm = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('sync timeout')), timeoutMs);
});
syncConnection = await Promise.race([syncProm, timeoutProm]);
const content = syncConnection.read(filename);
let result_string = '';
for await (const chunk of content) {
result_string += new TextDecoder().decode(chunk);
}
return result_string;
}
} catch (pull_error: any) {
console.log('pulling error', pull_error);
} finally {
await cleanupSync();
}
});
}
export async function push(path: string, obj: string) {
let instance = getAdbInstance();
if (instance) {
let sync = await instance.sync();
const encoder = new TextEncoder();
return await runSyncOperation(async () => {
let instance = getAdbInstance();
if (instance) {
let sync = await instance.sync();
const encoder = new TextEncoder();
const file: ReadableStream<MaybeConsumable<Uint8Array>> = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(encoder.encode(obj)));
controller.close();
}
});
try {
console.log('support push v2', sync.supportsSendReceiveV2);
await sync.write({
filename: path,
file
});
} catch (error) {
console.log('error while trying to write to machine', error);
} finally {
await sync.dispose();
}
}
});
}
// Push a binary file (e.g. an .mp4 video) to the machine. Unlike push() which
// text-encodes a string, this streams raw bytes in chunks so binary data is not
// corrupted, and reports progress.
export async function pushBinary(
path: string,
data: Uint8Array,
onProgress?: (sent: number, total: number) => void
): Promise<boolean> {
return await runSyncOperation(async () => {
let instance = getAdbInstance();
if (!instance) return false;
const total = data.byteLength;
onProgress?.(0, total);
let sync = await instance.sync();
// Mirror the working text push(): a single enqueue then close. @yume-chan
// packetizes internally for the ADB protocol; a multi-chunk or pull-based
// stream can stall the transfer.
const file: ReadableStream<MaybeConsumable<Uint8Array>> = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(encoder.encode(obj)));
controller.enqueue(data);
controller.close();
}
});
try {
console.log('support push v2', sync.supportsSendReceiveV2);
await sync.write({
filename: path,
file
});
const writeProm = sync.write({ filename: path, file });
// Safety net so a stalled transfer can't hang the UI forever.
const timeoutProm = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('push write timeout (120s)')), 120000)
);
await Promise.race([writeProm, timeoutProm]);
onProgress?.(total, total);
return true;
} catch (error) {
console.log('error while trying to write to machine', error);
console.log('error while pushing binary to machine', error);
return false;
} finally {
await sync.dispose();
}
}
});
}
// NOTE: adb reverse is not work by unavailable features support
export async function reconnectAndroidServer() {
await connectToAndroidServer();
}
async function connectToAndroidServer(maxRetries = 5) {
let lastError: any;
for (let attempt = 0; attempt < maxRetries; attempt++) {
@ -300,10 +508,12 @@ async function connectToAndroidServer(maxRetries = 5) {
return;
}
const brewConnectionPort = env.PUBLIC_BREW_CONN_PORT || 'tcp:36588';
// add retry mechanism
const stream = await connectWithRetry(
async () => inst.transport.connect(env.PUBLIC_BREW_CONN_PORT),
`connect to Android server port ${env.PUBLIC_BREW_CONN_PORT}`,
async () => inst.transport.connect(brewConnectionPort),
`connect to Android server port ${brewConnectionPort}`,
3,
500
);
@ -316,22 +526,24 @@ async function connectToAndroidServer(maxRetries = 5) {
if (writer) {
addNotification('INFO:Enable Brewing Mode T on machine');
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
handleAdbPayload(new TextDecoder().decode(value));
(async () => {
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
handleAdbPayload(new TextDecoder().decode(value));
}
} catch (e) {
console.error('read error', e);
if (isRecoverableError(e)) {
void connectToAndroidServer();
}
} finally {
adbWriter.set(null);
addNotification('WARN:Brewing Mode T Offline ...');
}
} catch (e) {
console.error('read error', e);
if (isRecoverableError(e)) {
throw e;
}
throw e;
} finally {
adbWriter.set(null);
addNotification('WARN:Brewing Mode T Offline ...');
}
})();
return;
} else {
addNotification('WARN:Brewing Mode T unavailable');
@ -366,6 +578,94 @@ async function connectToAndroidServer(maxRetries = 5) {
}
}
async function connectToAndroidRecipeMenuServer(notifyFailure = true, retryOnFailure = false) {
if (recipeMenuAndroidServerConnectPromise) return recipeMenuAndroidServerConnectPromise;
recipeMenuAndroidServerConnectPromise = connectToAndroidRecipeMenuServerOnce(
notifyFailure,
retryOnFailure
).finally(() => {
recipeMenuAndroidServerConnectPromise = null;
});
return recipeMenuAndroidServerConnectPromise;
}
async function connectToAndroidRecipeMenuServerOnce(notifyFailure = true, retryOnFailure = false) {
try {
let inst = getAdbInstance();
if (!inst) {
console.warn('recipe menu adb instance not found');
return;
}
const brewConnectionPort = env.PUBLIC_BREW_CONN_PORT || 'tcp:36588';
clearRecipeMenuAndroidServerRetry();
const stream = await inst.transport.connect(brewConnectionPort);
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
console.log('checking recipe menu writer ', writer);
adbWriter.set(writer);
if (writer) {
addNotification('INFO:Enable Android recipe menu channel');
} else {
addNotification('WARN:Android recipe menu channel unavailable');
setTimeout(async () => {
console.log('reconnecting android recipe menu server');
await connectToAndroidRecipeMenuServer();
}, 5000);
}
(async () => {
try {
let messageBuffer = '';
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const decoded = decoder.decode(value, { stream: true });
console.log('[ADB Reader] Received raw:', decoded.slice(0, 200));
messageBuffer += decoded;
const messages = messageBuffer.split('\n');
messageBuffer = messages.pop() ?? '';
for (const message of messages) {
const trimmedMessage = message.trim();
if (trimmedMessage) {
console.log('[ADB Reader] Processing message:', trimmedMessage.slice(0, 200));
handleAdbPayload(trimmedMessage);
}
}
}
const remainingMessage = messageBuffer.trim();
if (remainingMessage) {
handleAdbPayload(remainingMessage);
}
} catch (e) {
console.error('recipe menu read error', e);
} finally {
adbWriter.set(null);
addNotification('WARN:Android recipe menu channel offline ...');
if (retryOnFailure) {
scheduleRecipeMenuAndroidServerReconnect();
}
}
})();
} catch (err) {
console.error('Recipe menu connection failed. Suspect java running or not', err);
adbWriter.set(null);
if (notifyFailure) addNotification('ERR:Fail to enable Android recipe menu channel');
if (retryOnFailure) {
scheduleRecipeMenuAndroidServerReconnect();
}
}
}
// logcat stream
// TODO: screen mirror