feat: add brew app connection
- initialize tcp communication with brew app - WIP value editor sync Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
parent
08f7626dcb
commit
274025ed33
14 changed files with 431 additions and 69 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -19,7 +19,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "^4.1.3",
|
"@chromatic-com/storybook": "^4.1.3",
|
||||||
"@internationalized/date": "^3.10.1",
|
"@internationalized/date": "^3.12.0",
|
||||||
"@lucide/svelte": "^0.561.0",
|
"@lucide/svelte": "^0.561.0",
|
||||||
"@storybook/addon-a11y": "^10.2.0",
|
"@storybook/addon-a11y": "^10.2.0",
|
||||||
"@storybook/addon-docs": "^10.2.0",
|
"@storybook/addon-docs": "^10.2.0",
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
"@tanstack/table-core": "^8.21.3",
|
"@tanstack/table-core": "^8.21.3",
|
||||||
"@types/node": "^22.19.7",
|
"@types/node": "^22.19.7",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^3.2.4",
|
||||||
"bits-ui": "^2.15.4",
|
"bits-ui": "^2.16.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"paneforge": "^1.0.0-next.6",
|
"paneforge": "^1.0.0-next.6",
|
||||||
"playwright": "^1.57.0",
|
"playwright": "^1.57.0",
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,15 @@
|
||||||
import { handleIncomingMessages } from '$lib/core/handlers/messageHandler';
|
import { handleIncomingMessages } from '$lib/core/handlers/messageHandler';
|
||||||
import { auth as authStore } from '$lib/core/stores/auth';
|
import { auth as authStore } from '$lib/core/stores/auth';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb';
|
import {
|
||||||
|
AdbDaemonWebUsbDevice,
|
||||||
|
AdbDaemonWebUsbDeviceManager
|
||||||
|
} from '@yume-chan/adb-daemon-webusb';
|
||||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
|
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
|
||||||
import { file } from 'zod/mini';
|
import { file } from 'zod/mini';
|
||||||
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
|
|
||||||
let { enableComponent = true } = $props();
|
let { enableComponent = true } = $props();
|
||||||
|
|
||||||
|
|
@ -161,6 +165,9 @@
|
||||||
} else {
|
} else {
|
||||||
console.log('push pull not ok', result);
|
console.log('push pull not ok', result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clean file
|
||||||
|
await adb.executeCmd('rm /sdcard/coffeevending/test.json');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('test push file failed', error);
|
console.log('test push file failed', error);
|
||||||
|
|
@ -205,6 +212,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disconnectAdb() {
|
async function disconnectAdb() {
|
||||||
|
// clean up no password flag
|
||||||
|
// return to engine
|
||||||
|
|
||||||
await adb.disconnect();
|
await adb.disconnect();
|
||||||
connectionButtonText = 'Connect';
|
connectionButtonText = 'Connect';
|
||||||
connectionButtonVariant = 'default';
|
connectionButtonVariant = 'default';
|
||||||
|
|
@ -313,6 +323,12 @@
|
||||||
} catch (ignored) {}
|
} catch (ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
||||||
|
addNotification(
|
||||||
|
'ERR:Device is already in use by another program, please close the program and try again'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
import * as Card from '$lib/components/ui/card/index';
|
import * as Card from '$lib/components/ui/card/index';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
import Input from '$lib/components/ui/input/input.svelte';
|
import Input from '$lib/components/ui/input/input.svelte';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||||
import RecipelistTable from './recipelist-table.svelte';
|
import RecipelistTable from './recipelist-table.svelte';
|
||||||
|
|
||||||
|
import * as adb from '$lib/core/adb/adb';
|
||||||
import { columns, type RecipelistMaterial } from './columns';
|
import { columns, type RecipelistMaterial } from './columns';
|
||||||
import { get, readable, writable } from 'svelte/store';
|
import { get, readable, writable } from 'svelte/store';
|
||||||
import {
|
import {
|
||||||
|
|
@ -17,6 +19,11 @@
|
||||||
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
|
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
|
||||||
import MachineInfo from '../machine-info.svelte';
|
import MachineInfo from '../machine-info.svelte';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
import { sendCommand, sendReset } from '$lib/core/brew/command';
|
||||||
|
import { isAdbWriterAvailable } from '$lib/core/stores/adbWriter';
|
||||||
|
import { sendToAndroid } from '$lib/core/stores/adbWriter';
|
||||||
|
|
||||||
//
|
//
|
||||||
let {
|
let {
|
||||||
|
|
@ -35,6 +42,8 @@
|
||||||
|
|
||||||
let toppingSlotState: any = $state([]);
|
let toppingSlotState: any = $state([]);
|
||||||
|
|
||||||
|
const recipeDetailDispatch = createEventDispatcher();
|
||||||
|
|
||||||
function remappingToColumn(data: any[]): RecipelistMaterial[] {
|
function remappingToColumn(data: any[]): RecipelistMaterial[] {
|
||||||
let ret: RecipelistMaterial[] = [];
|
let ret: RecipelistMaterial[] = [];
|
||||||
// expect recipelist
|
// expect recipelist
|
||||||
|
|
@ -90,6 +99,62 @@
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getCurrentQueue() {
|
||||||
|
let inst = adb.getAdbInstance();
|
||||||
|
if (inst) {
|
||||||
|
let current_brewing = await adb.pull(env.PUBLIC_BREW_CURRENT_RECIPE);
|
||||||
|
// console.log(`current brewing queue: ${current_brewing}`);
|
||||||
|
if (current_brewing === '') {
|
||||||
|
current_brewing = '{}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(current_brewing ?? '{}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: 'instance lost'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetAllPendingCmds() {
|
||||||
|
// send reset to brew
|
||||||
|
try {
|
||||||
|
await sendReset();
|
||||||
|
addNotification(`INFO:Reset completed!`);
|
||||||
|
} catch (e) {
|
||||||
|
addNotification(`ERR:${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveRecipe() {}
|
||||||
|
|
||||||
|
async function sendTriggerBrewNow() {
|
||||||
|
// check queue ready
|
||||||
|
// let currentBrewingQueue = await getCurrentQueue();
|
||||||
|
// console.log('checking queue ... ', Object.keys(currentBrewingQueue).length);
|
||||||
|
|
||||||
|
await sendToAndroid({
|
||||||
|
type: 'brew_prep',
|
||||||
|
payload: {
|
||||||
|
start: new Date().toLocaleTimeString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if (Object.keys(currentBrewingQueue).length != 0) {
|
||||||
|
// addNotification('ERR:Brewing queue is full, please check machine or press `reset`');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
//
|
||||||
|
let inst = adb.getAdbInstance();
|
||||||
|
if (inst) {
|
||||||
|
console.log('check adb writer', isAdbWriterAvailable());
|
||||||
|
recipeDetailDispatch('brewNow');
|
||||||
|
} else {
|
||||||
|
console.log('result check fail');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkChanges(original: any) {
|
async function checkChanges(original: any) {
|
||||||
console.log('old', original, 'updated', recipeListMatState);
|
console.log('old', original, 'updated', recipeListMatState);
|
||||||
if (recipeListOriginal.length == 0) {
|
if (recipeListOriginal.length == 0) {
|
||||||
|
|
@ -133,10 +198,23 @@
|
||||||
|
|
||||||
<div class="-mb-4 flex w-full flex-col gap-6">
|
<div class="-mb-4 flex w-full flex-col gap-6">
|
||||||
<Tabs.Root value="info">
|
<Tabs.Root value="info">
|
||||||
<Tabs.List>
|
<div class="flex flex-row justify-between">
|
||||||
<Tabs.Trigger value="info">Info</Tabs.Trigger>
|
<Tabs.List>
|
||||||
<Tabs.Trigger value="details">Details</Tabs.Trigger>
|
<Tabs.Trigger value="info">Info</Tabs.Trigger>
|
||||||
</Tabs.List>
|
<Tabs.Trigger value="details">Details</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
{#if refPage === 'brew'}
|
||||||
|
<div>
|
||||||
|
<Button type="button" variant="default" onclick={() => resetAllPendingCmds()}
|
||||||
|
>Force Reset Brewing</Button
|
||||||
|
>
|
||||||
|
<Button type="button" variant="default" onclick={() => saveRecipe()}>Save</Button>
|
||||||
|
<Button type="button" variant="default" onclick={async () => sendTriggerBrewNow()}
|
||||||
|
>Test Brew</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Tabs.Content value="info">
|
<Tabs.Content value="info">
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,8 @@
|
||||||
currentRef
|
currentRef
|
||||||
);
|
);
|
||||||
|
|
||||||
|
sheetOpenState = false;
|
||||||
|
} else if (value_event_state === ValueEvent.NONE) {
|
||||||
sheetOpenState = false;
|
sheetOpenState = false;
|
||||||
} else {
|
} else {
|
||||||
// set noti
|
// set noti
|
||||||
|
|
@ -296,7 +298,7 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
sheetOpenState = false;
|
sheetOpenState = false;
|
||||||
console.log('sheet open? ', sheetOpenState);
|
// console.log('sheet open? ', sheetOpenState);
|
||||||
|
|
||||||
let refFrom = get(referenceFromPage);
|
let refFrom = get(referenceFromPage);
|
||||||
categories = get(
|
categories = get(
|
||||||
|
|
@ -317,16 +319,7 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sheet.Root
|
<Sheet.Root>
|
||||||
bind:open={sheetOpenState}
|
|
||||||
onOpenChange={(next) => {
|
|
||||||
if (!next) {
|
|
||||||
beforeClosing();
|
|
||||||
} else {
|
|
||||||
sheetOpenState = true;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Sheet.Trigger>
|
<Sheet.Trigger>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|
@ -616,12 +609,12 @@
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<!-- final -->
|
<!-- final -->
|
||||||
<Field.Field orientation="horizontal">
|
<!-- <Field.Field orientation="horizontal">
|
||||||
<Button type="button" onclick={() => saveEditingValue()}>Save</Button>
|
<Button type="button" onclick={() => saveEditingValue()}>Save</Button>
|
||||||
<Button variant="outline" type="button" onclick={() => beforeClosing()}
|
<Button variant="outline" type="button" onclick={() => (sheetOpenState = false)}
|
||||||
>{warnUserNotSaveChange ? 'Discard Changes' : 'Cancel'}</Button
|
>{warnUserNotSaveChange ? 'Discard Changes' : 'Cancel'}</Button
|
||||||
>
|
>
|
||||||
</Field.Field>
|
</Field.Field> -->
|
||||||
</Field.Group>
|
</Field.Group>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
let key = kv[0];
|
let key = kv[0];
|
||||||
let value = kv[1] ?? '';
|
let value = kv[1] ?? '';
|
||||||
|
|
||||||
console.log('key', key, 'value', value);
|
// console.log('key', key, 'value', value);
|
||||||
currentStringParams[key] = value;
|
currentStringParams[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,9 +204,9 @@
|
||||||
console.log('detect mix order', mat_num);
|
console.log('detect mix order', mat_num);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feed.parameter > 0 || feed.pattern > 0) {
|
// if (feed.parameter > 0 || feed.pattern > 0) {
|
||||||
console.log('has feed fields', JSON.stringify(feed));
|
// console.log('has feed fields', JSON.stringify(feed));
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@
|
||||||
import { MenuStatus, matchMenuStatus } from '$lib/core/types/menuStatus';
|
import { MenuStatus, matchMenuStatus } from '$lib/core/types/menuStatus';
|
||||||
|
|
||||||
import * as adb from '$lib/core/adb/adb';
|
import * as adb from '$lib/core/adb/adb';
|
||||||
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
let open = $state(false);
|
import { sendToAndroid } from '$lib/core/stores/adbWriter';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
const isDesktop = new MediaQuery('(min-width: 768px)');
|
const isDesktop = new MediaQuery('(min-width: 768px)');
|
||||||
|
|
||||||
let currentData: any = $state();
|
let currentData: any = $state();
|
||||||
|
|
@ -27,6 +28,8 @@
|
||||||
refPage: string;
|
refPage: string;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
let ready_to_send_brew: any[] = $state([]);
|
||||||
|
|
||||||
async function onPendingChange(newChange: { target: string; value: any }) {
|
async function onPendingChange(newChange: { target: string; value: any }) {
|
||||||
console.log('detect pending change', matchMenuStatus(currentData.MenuStatus));
|
console.log('detect pending change', matchMenuStatus(currentData.MenuStatus));
|
||||||
hasPendingChange = true;
|
hasPendingChange = true;
|
||||||
|
|
@ -51,6 +54,9 @@
|
||||||
console.log('pending change recipe list', newChange);
|
console.log('pending change recipe list', newChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ready_to_send_brew = [];
|
||||||
|
ready_to_send_brew.push([env.PUBLIC_BREW_CURRENT_RECIPE, JSON.stringify(currentData)]);
|
||||||
|
|
||||||
// await adb.push('/sdcard/coffeevending/.curr.brewing.json', JSON.stringify(currentData));
|
// await adb.push('/sdcard/coffeevending/.curr.brewing.json', JSON.stringify(currentData));
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
|
|
@ -59,6 +65,23 @@
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendBrewNow() {
|
||||||
|
try {
|
||||||
|
await sendToAndroid({
|
||||||
|
type: 'brew',
|
||||||
|
payload: {
|
||||||
|
start: new Date().toLocaleTimeString(),
|
||||||
|
// use this field for unchanged data
|
||||||
|
target: ready_to_send_brew.length == 1 ? '-' : currentData.productCode,
|
||||||
|
// use this field for new or modified data
|
||||||
|
data: ready_to_send_brew.length == 1 ? ready_to_send_brew[0][1] : null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
addNotification(`ERR:Failed to brewing\n${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
//
|
//
|
||||||
if (refPage === 'brew') {
|
if (refPage === 'brew') {
|
||||||
|
|
@ -86,7 +109,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isDesktop.current}
|
{#if isDesktop.current}
|
||||||
<Dialog.Root bind:open>
|
<Dialog.Root>
|
||||||
<Dialog.Trigger class="w-full text-start" onselect={(e) => e.preventDefault()}
|
<Dialog.Trigger class="w-full text-start" onselect={(e) => e.preventDefault()}
|
||||||
>View</Dialog.Trigger
|
>View</Dialog.Trigger
|
||||||
>
|
>
|
||||||
|
|
@ -100,7 +123,12 @@
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<!-- render more -->
|
<!-- render more -->
|
||||||
<RecipeDetail recipeData={currentData} {onPendingChange} {refPage} />
|
<RecipeDetail
|
||||||
|
recipeData={currentData}
|
||||||
|
{onPendingChange}
|
||||||
|
{refPage}
|
||||||
|
on:brewNow={async () => sendBrewNow()}
|
||||||
|
/>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,21 @@
|
||||||
import { Adb, AdbDaemonTransport, encodeUtf8 } from '@yume-chan/adb';
|
import { Adb, AdbDaemonTransport, encodeUtf8 } from '@yume-chan/adb';
|
||||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
||||||
import {
|
import {
|
||||||
|
AdbDaemonWebUsbDevice,
|
||||||
AdbDaemonWebUsbDeviceManager,
|
AdbDaemonWebUsbDeviceManager,
|
||||||
AdbDaemonWebUsbDeviceObserver,
|
AdbDaemonWebUsbDeviceObserver
|
||||||
type AdbDaemonWebUsbDevice
|
|
||||||
} from '@yume-chan/adb-daemon-webusb';
|
} from '@yume-chan/adb-daemon-webusb';
|
||||||
import { AdbInstance } from '../../../routes/state.svelte';
|
import { AdbInstance } from '../../../routes/state.svelte';
|
||||||
import { deviceCredentialManager } from './deviceCredManager';
|
import { deviceCredentialManager } from './deviceCredManager';
|
||||||
import { Consumable, MaybeConsumable, ReadableStream } from '@yume-chan/stream-extra';
|
import { Consumable, MaybeConsumable, ReadableStream } from '@yume-chan/stream-extra';
|
||||||
import { AdbScrcpyClient } from '@yume-chan/adb-scrcpy';
|
import { AdbScrcpyClient } from '@yume-chan/adb-scrcpy';
|
||||||
|
import { addNotification } from '../stores/noti';
|
||||||
|
import { handleAdbPayload } from '../handlers/adbPayloadHandler';
|
||||||
|
import { adbWriter } from '../stores/adbWriter';
|
||||||
|
import { WritableStream } from '@yume-chan/stream-extra';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
|
let syncConnection: any = null;
|
||||||
|
|
||||||
export async function connnectViaWebUSB() {
|
export async function connnectViaWebUSB() {
|
||||||
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
|
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
|
||||||
|
|
@ -27,13 +34,21 @@ export async function connnectViaWebUSB() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const adb = new Adb(transport);
|
const adb = new Adb(transport);
|
||||||
saveAdbInstance(adb);
|
await saveAdbInstance(adb);
|
||||||
|
await connectToAndroidServer();
|
||||||
|
|
||||||
// save device info
|
// save device info
|
||||||
await deviceCredentialManager.saveDeviceInfo(device);
|
await deviceCredentialManager.saveDeviceInfo(device);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('error on connect', e);
|
console.error('error on connect', e);
|
||||||
throw new Error(e.toString());
|
|
||||||
|
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
||||||
|
addNotification(
|
||||||
|
'ERR:Device is already in use by another program, please close the program and try again'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +66,9 @@ export async function connectDeviceByCred(
|
||||||
});
|
});
|
||||||
|
|
||||||
const adb = new Adb(transport);
|
const adb = new Adb(transport);
|
||||||
saveAdbInstance(adb);
|
|
||||||
|
await saveAdbInstance(adb);
|
||||||
|
await connectToAndroidServer();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -59,7 +76,8 @@ export async function connectDeviceByCred(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveAdbInstance(adb: Adb | undefined) {
|
export async function saveAdbInstance(adb: Adb | undefined) {
|
||||||
|
await cleanupSync();
|
||||||
AdbInstance.instance = adb;
|
AdbInstance.instance = adb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,11 +118,12 @@ export async function executeCmd(command: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log(e.message);
|
// console.log(e.message);
|
||||||
//ExactReadable ended
|
//ExactReadable ended
|
||||||
if (e.message.includes('ExactReadable ended')) {
|
if (e.message.includes('ExactReadable ended')) {
|
||||||
console.error('connection cut off');
|
|
||||||
return {
|
return {
|
||||||
|
output: '',
|
||||||
|
exitCode: 1,
|
||||||
error: 'ExactReadableEndedError'
|
error: 'ExactReadableEndedError'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -119,32 +138,49 @@ export async function disconnect() {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
await instance.close();
|
await instance.close();
|
||||||
console.log('close instance');
|
console.log('close instance');
|
||||||
saveAdbInstance(undefined);
|
await saveAdbInstance(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pull(filename: string) {
|
export async function cleanupSync() {
|
||||||
let instance = getAdbInstance();
|
if (syncConnection) {
|
||||||
if (instance) {
|
try {
|
||||||
let chunkList: Uint8Array<ArrayBufferLike>[] = [];
|
await syncConnection.dispose();
|
||||||
let sync = await instance.sync();
|
} catch (e) {
|
||||||
const content = sync.read(filename);
|
console.error('error on dispose sync', e);
|
||||||
let result = content.values();
|
|
||||||
let res;
|
|
||||||
|
|
||||||
let result_string = '';
|
|
||||||
|
|
||||||
while ((res = await result.next()) != null) {
|
|
||||||
// console.log(res.value);
|
|
||||||
if (res.value != undefined) {
|
|
||||||
result_string += new TextDecoder().decode(res.value);
|
|
||||||
}
|
|
||||||
if (res.done) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result_string;
|
syncConnection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pull(filename: string, timeoutMs: number = 5000) {
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,6 +212,38 @@ export async function push(path: string, obj: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: adb reverse is not work by unavailable features support
|
||||||
|
async function connectToAndroidServer() {
|
||||||
|
try {
|
||||||
|
let inst = getAdbInstance();
|
||||||
|
if (!inst) {
|
||||||
|
console.warn('adb instance not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stream = await inst.transport.connect(env.PUBLIC_BREW_CONN_PORT);
|
||||||
|
const writer = stream.writable.getWriter();
|
||||||
|
const reader = stream.readable.getReader();
|
||||||
|
|
||||||
|
adbWriter.set(writer);
|
||||||
|
|
||||||
|
(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);
|
||||||
|
} finally {
|
||||||
|
adbWriter.set(null);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Connection failed. Suspect java running or not', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// logcat stream
|
// logcat stream
|
||||||
|
|
||||||
// TODO: screen mirror
|
// TODO: screen mirror
|
||||||
|
|
|
||||||
46
src/lib/core/brew/command.ts
Normal file
46
src/lib/core/brew/command.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
import * as adb from '$lib/core/adb/adb';
|
||||||
|
|
||||||
|
class BrewCommandError extends Error {
|
||||||
|
public readonly field?: string;
|
||||||
|
|
||||||
|
constructor(message: string, field?: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'BrewCommandError';
|
||||||
|
this.field = field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send command to brew app
|
||||||
|
/// NOTE: app must enable flag `enable_adb_block_watch`
|
||||||
|
async function sendCommand(type: string, params?: string[]) {
|
||||||
|
// check instance
|
||||||
|
let inst = adb.getAdbInstance();
|
||||||
|
if (inst) {
|
||||||
|
try {
|
||||||
|
let cmd = type + ' ' + (params?.join(' ') ?? '');
|
||||||
|
await adb.push(env.PUBLIC_BREW_CMD_WEB, cmd);
|
||||||
|
} catch (e) {
|
||||||
|
throw new BrewCommandError('Command failed', `${e}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BrewCommandError('Instance lost');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendReset() {
|
||||||
|
let inst = adb.getAdbInstance();
|
||||||
|
if (inst) {
|
||||||
|
try {
|
||||||
|
await adb.push(env.PUBLIC_BREW_CMD_WEB, '');
|
||||||
|
await adb.push(env.PUBLIC_BREW_CURRENT_RECIPE, '');
|
||||||
|
await adb.push(env.PUBLIC_BREW_WEB_STATUS, '');
|
||||||
|
} catch (e) {
|
||||||
|
throw new BrewCommandError('Reset failed', `${e}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BrewCommandError('Instance lost');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { sendCommand, sendReset, BrewCommandError };
|
||||||
41
src/lib/core/handlers/adbPayloadHandler.ts
Normal file
41
src/lib/core/handlers/adbPayloadHandler.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { addNotification } from '../stores/noti';
|
||||||
|
|
||||||
|
type AdbPayload = { type: string; payload: any };
|
||||||
|
|
||||||
|
async function handleAdbPayload(raw_payload: string) {
|
||||||
|
console.log('get payload', raw_payload);
|
||||||
|
try {
|
||||||
|
const payload: AdbPayload = JSON.parse(raw_payload);
|
||||||
|
switch (payload.type) {
|
||||||
|
case 'log':
|
||||||
|
let log_level = payload.payload['level'] ?? 'INFO';
|
||||||
|
let log_message = payload.payload['msg'] ?? '';
|
||||||
|
|
||||||
|
if (log_message !== '') addNotification(`${log_level}`);
|
||||||
|
break;
|
||||||
|
case 'response':
|
||||||
|
if (payload.payload instanceof String) {
|
||||||
|
// single message response
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ACK':
|
||||||
|
// acknowledge response from app
|
||||||
|
if (payload.payload !== 'OK') {
|
||||||
|
// abnormal
|
||||||
|
console.error('error from ACK', payload.payload);
|
||||||
|
addNotification('ERR:Request rejected');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
// show error to user from brew app
|
||||||
|
addNotification(`ERR:${payload.payload}`);
|
||||||
|
// send message to server if needed
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// invalid format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handleAdbPayload };
|
||||||
28
src/lib/core/stores/adbWriter.ts
Normal file
28
src/lib/core/stores/adbWriter.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
import { addNotification } from './noti';
|
||||||
|
|
||||||
|
const adbWriter: any = writable(null);
|
||||||
|
|
||||||
|
async function sendToAndroid(message: any) {
|
||||||
|
let writer: any = get(adbWriter);
|
||||||
|
console.log('writer', writer);
|
||||||
|
if (!writer) {
|
||||||
|
addNotification('ERR:No active connection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
console.log(writer);
|
||||||
|
await writer.write(encoder.encode(JSON.stringify(message) + '\n'));
|
||||||
|
console.log('sent!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('write failed', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function for checking if connection is ok
|
||||||
|
function isAdbWriterAvailable() {
|
||||||
|
return get(adbWriter) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { sendToAndroid, adbWriter, isAdbWriterAvailable };
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export function update
|
|
||||||
|
|
@ -47,6 +47,12 @@
|
||||||
} catch (ignored) {}
|
} catch (ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
||||||
|
addNotification(
|
||||||
|
'ERR:Device is already in use by another program, please close the program and try again'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,14 @@
|
||||||
import { auth as authStore } from '$lib/core/stores/auth';
|
import { auth as authStore } from '$lib/core/stores/auth';
|
||||||
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
|
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb';
|
import {
|
||||||
|
AdbDaemonWebUsbDevice,
|
||||||
|
AdbDaemonWebUsbDeviceManager
|
||||||
|
} from '@yume-chan/adb-daemon-webusb';
|
||||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
|
||||||
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
|
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
|
||||||
import { afterNavigate } from '$app/navigation';
|
import { afterNavigate } from '$app/navigation';
|
||||||
|
import { env } from '$env/dynamic/public';
|
||||||
|
|
||||||
const sourceDir = '/sdcard/coffeevending';
|
const sourceDir = '/sdcard/coffeevending';
|
||||||
|
|
||||||
|
|
@ -35,15 +39,17 @@
|
||||||
recipes: []
|
recipes: []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let brew_status: string = $state('');
|
||||||
|
|
||||||
async function startFetchRecipeFromMachine() {
|
async function startFetchRecipeFromMachine() {
|
||||||
let instance = adb.getAdbInstance();
|
let instance = adb.getAdbInstance();
|
||||||
recipeFromMachineLoading.set(true);
|
// recipeFromMachineLoading.set(true);
|
||||||
referenceFromPage.set('brew');
|
referenceFromPage.set('brew');
|
||||||
console.log('check instance', instance);
|
console.log('check instance', instance);
|
||||||
if (instance) {
|
if (instance) {
|
||||||
console.log('instance passed!');
|
console.log('instance passed!');
|
||||||
let dev_recipe = await adb.pull(`${sourceDir}/cfg/recipe_branch_dev.json`);
|
let dev_recipe = await adb.pull(`${sourceDir}/cfg/recipe_branch_dev.json`);
|
||||||
// console.log('dev recipe ok', dev_recipe);
|
console.log('dev recipe ok', dev_recipe != undefined);
|
||||||
if (dev_recipe) {
|
if (dev_recipe) {
|
||||||
if (dev_recipe.length == 0) {
|
if (dev_recipe.length == 0) {
|
||||||
// case error, do last retry
|
// case error, do last retry
|
||||||
|
|
@ -54,7 +60,7 @@
|
||||||
else if (dev_recipe) {
|
else if (dev_recipe) {
|
||||||
// From coffeethai02
|
// From coffeethai02
|
||||||
devRecipe = JSON.parse(dev_recipe);
|
devRecipe = JSON.parse(dev_recipe);
|
||||||
recipeFromMachineLoading.set(false);
|
// recipeFromMachineLoading.set(false);
|
||||||
addNotification('INFO:Fetch recipe success!');
|
addNotification('INFO:Fetch recipe success!');
|
||||||
|
|
||||||
buildOverviewForBrewing();
|
buildOverviewForBrewing();
|
||||||
|
|
@ -62,7 +68,7 @@
|
||||||
} else {
|
} else {
|
||||||
// from recipe_branch_dev
|
// from recipe_branch_dev
|
||||||
devRecipe = JSON.parse(dev_recipe);
|
devRecipe = JSON.parse(dev_recipe);
|
||||||
recipeFromMachineLoading.set(false);
|
// recipeFromMachineLoading.set(false);
|
||||||
addNotification('INFO:Fetch recipe success!');
|
addNotification('INFO:Fetch recipe success!');
|
||||||
|
|
||||||
buildOverviewForBrewing();
|
buildOverviewForBrewing();
|
||||||
|
|
@ -70,7 +76,7 @@
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
addNotification('ERROR:Cannot connect to machine');
|
addNotification('ERROR:Cannot connect to machine');
|
||||||
recipeFromMachineLoading.set(false);
|
// recipeFromMachineLoading.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,7 +119,7 @@
|
||||||
async function connectAdb() {
|
async function connectAdb() {
|
||||||
try {
|
try {
|
||||||
if (!('usb' in navigator)) {
|
if (!('usb' in navigator)) {
|
||||||
throw new Error('WebUSB not supported, try using fallback or different browser');
|
throw new Error('WebUSB not supported, try using fallback method or different browser');
|
||||||
}
|
}
|
||||||
|
|
||||||
await adb.connnectViaWebUSB();
|
await adb.connnectViaWebUSB();
|
||||||
|
|
@ -131,7 +137,7 @@
|
||||||
async function tryAutoConnect() {
|
async function tryAutoConnect() {
|
||||||
try {
|
try {
|
||||||
if (!('usb' in navigator) || !AdbDaemonWebUsbDeviceManager.BROWSER) {
|
if (!('usb' in navigator) || !AdbDaemonWebUsbDeviceManager.BROWSER) {
|
||||||
throw new Error('WebUSB not supported, try using fallback or different browser');
|
throw new Error('WebUSB not supported, try using fallback method or different browser');
|
||||||
}
|
}
|
||||||
|
|
||||||
const devices = await AdbDaemonWebUsbDeviceManager.BROWSER.getDevices();
|
const devices = await AdbDaemonWebUsbDeviceManager.BROWSER.getDevices();
|
||||||
|
|
@ -155,6 +161,11 @@
|
||||||
await deviceCredentialManager.clearAllCredentials();
|
await deviceCredentialManager.clearAllCredentials();
|
||||||
} catch (ignored) {}
|
} catch (ignored) {}
|
||||||
}
|
}
|
||||||
|
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
||||||
|
addNotification(
|
||||||
|
'ERR:Device is already in use by another program, please close the program and try again'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -320,6 +331,54 @@
|
||||||
toppingGroupFromMachineQuery.set(devRecipe['Topping']['ToppingGroup']);
|
toppingGroupFromMachineQuery.set(devRecipe['Topping']['ToppingGroup']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const brewAppStatusInterval = setInterval(async () => {
|
||||||
|
// schedule status from .brew_web_status.log
|
||||||
|
let inst = adb.getAdbInstance();
|
||||||
|
if (inst && devRecipe) {
|
||||||
|
await adb.executeCmd(
|
||||||
|
'tail -n 1 /sdcard/coffeevending/.brew_web_status.log > /sdcard/coffeevending/.brew_web_status.latest.log'
|
||||||
|
);
|
||||||
|
|
||||||
|
let brew_status_log = await adb.pull(env.PUBLIC_BREW_WEB_LATEST_STATUS);
|
||||||
|
if (brew_status_log) {
|
||||||
|
let latest_log = brew_status_log;
|
||||||
|
|
||||||
|
if (brew_status !== latest_log) {
|
||||||
|
brew_status = latest_log;
|
||||||
|
|
||||||
|
// noti from machine
|
||||||
|
if (latest_log.includes('!!!')) {
|
||||||
|
let log = latest_log.split('!!!');
|
||||||
|
let noti_cfg = log[1];
|
||||||
|
|
||||||
|
if (noti_cfg.includes('=')) {
|
||||||
|
let noti_level = noti_cfg.split('=')[1];
|
||||||
|
|
||||||
|
let spl = log[0].split(':');
|
||||||
|
let pure_msg = spl[spl.length - 1];
|
||||||
|
|
||||||
|
// case special message
|
||||||
|
if (pure_msg.includes('starting retry process')) {
|
||||||
|
// is waiting/idle process
|
||||||
|
pure_msg = 'Wait for brewing';
|
||||||
|
}
|
||||||
|
|
||||||
|
addNotification(`${noti_level}:${pure_msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (inst && !devRecipe) {
|
||||||
|
// try again
|
||||||
|
// await startFetchRecipeFromMachine();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(brewAppStatusInterval);
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-8 flex">
|
<div class="mx-8 flex">
|
||||||
|
|
@ -333,7 +392,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-8 my-4 flex gap-2">
|
<div class="mx-8 my-4 flex gap-2">
|
||||||
{#if !adb.getAdbInstance()}
|
{#if !adb.getAdbInstance() || !devRecipe}
|
||||||
<Button variant="default" onclick={() => connectAdb()}>Connect</Button>
|
<Button variant="default" onclick={() => connectAdb()}>Connect</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button variant="default">+ Create Menu</Button>
|
<Button variant="default">+ Create Menu</Button>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue