feat: show price (WIP edit)

- fix: adb tcp connection unstable retry
- fix: recipe not show by undefined machine status

Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
pakintada@gmail.com 2026-05-18 09:49:09 +07:00
parent 60424ebe5a
commit 3b70cc9fe8
8 changed files with 269 additions and 48 deletions

View file

@ -17,7 +17,8 @@
currentEditingRecipeProductCode,
latestRecipeToppingData,
materialFromMachineQuery,
materialFromServerQuery
materialFromServerQuery,
priceRecipeData
} from '$lib/core/stores/recipeStore';
import { generateIcing } from '$lib/helpers/icingGen';
import { getMachineStatus, machineInfoStore } from '$lib/core/stores/machineInfoStore';
@ -28,6 +29,7 @@
import { sendCommand, sendReset } from '$lib/core/brew/command';
import { isAdbWriterAvailable } from '$lib/core/stores/adbWriter';
import { sendToAndroid } from '$lib/core/stores/adbWriter';
import { departmentStore } from '$lib/core/stores/departments';
//
let {
@ -38,6 +40,8 @@
}: { recipeData: any; onPendingChange: any; productCode: string; refPage: string } = $props();
let menuName: string = $state('');
let menuCurrentPrice: number = $state(0);
let isMenuHideByPrice: boolean = $state(false);
let materialSnapshot: any = $state();
let machineInfoSnapshot: any = $state();
@ -197,6 +201,35 @@
currentEditingRecipeProductCode.set(productCode);
let currentPricesFromServer = get(priceRecipeData);
let currentPrice = currentPricesFromServer[productCode] ?? '';
console.log(currentPricesFromServer);
if (currentPrice != '') {
// has price
let priceParts = currentPrice.split(',');
console.log('price part', priceParts);
try {
let price = parseInt(priceParts[0]);
let extraParam: string = priceParts[1] ?? '';
if (extraParam != '') {
isMenuHideByPrice = extraParam.includes('hide=true');
}
console.log('hide = ', extraParam);
menuCurrentPrice = price;
let dep = get(departmentStore);
if (dep && dep != 'tha') {
menuCurrentPrice = menuCurrentPrice / 100;
}
} catch (e) {}
}
// save old value\
}
});
@ -239,7 +272,16 @@
<Input id="tabs-menu-other-name" value={recipeData['otherName'] ?? ''} />
</div>
<div class="grid gap-3"></div>
<div class="grid gap-3">
<!-- price -->
<Label for="tabs-menu-price"
>Price
{#if isMenuHideByPrice}
<b> Disabled </b>
{/if}
</Label>
<Input id="tabs-menu-price" value={menuCurrentPrice} />
</div>
</Card.Content>
</Card.Root>
</Tabs.Content>

View file

@ -308,11 +308,21 @@
// interval check 1s
// machine
interval_get_machine_status = setInterval(() => {
if (getMachineStatus() == undefined) {
if (
getMachineStatus() == undefined ||
getMachineStatus() == null ||
$machineInfoStore?.status === undefined
) {
// set default now
updateMachineStatus('');
}
console.log(
'machine status pinging recipe editor dialog',
getMachineStatus(),
$machineInfoStore?.status
);
// update machine status
// check-connection
sendToAndroid({
@ -342,7 +352,7 @@
{#if $machineInfoStore?.status == 'IDLE' || $machineInfoStore?.status == '' || refPage == 'overview'}
Ready
{:else}
<Spinner /> Working
<Spinner /> Working {$machineInfoStore?.status}
{/if}
</Badge>
</Dialog.Title>

View file

@ -14,9 +14,83 @@ 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';
let syncConnection: any = null;
function isRecoverableError(error: any): boolean {
if (!error) return false;
const errorMessage = error.message ? String(error.message).toLowerCase() : '';
const errorName = error.name ? String(error.name).toLowerCase() : '';
// Network-related errors that are typically recoverable
const recoverablePatterns = [
'connection refused',
'connection reset',
'connection timeout',
'network is unreachable',
'host is unreachable',
'temporary failure',
'operation timed out',
'failed to connect',
'connection lost',
'broken pipe',
'socket closed',
'eof',
'end of file',
'disconnected'
];
for (const pattern of recoverablePatterns) {
if (errorMessage.includes(pattern) || errorName.includes(pattern)) {
return true;
}
}
if (
(error.name && error.name.includes('Error')) ||
error.name.includes('Exception') ||
error.name === 'IOError' ||
error.name === 'NetworkError'
) {
return true;
}
return false;
}
async function connectWithRetry<T>(
connectionFn: () => Promise<T>,
description: string,
maxRetries: number = 5,
baseDelayMs: number = 1000
): Promise<T> {
let lastError: any;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Connection timeout for ${description}`)), 10000);
});
const result = await Promise.race([connectionFn(), timeoutPromise]);
return result;
} catch (e) {
lastError = e;
if (attempt === maxRetries - 1) {
break;
}
if (!isRecoverableError(e)) {
break;
}
const delay = Math.min(baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000, 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error(
`failed to ${description} after ${maxRetries} attempts. Last error: ${lastError.message}`
);
}
export async function connnectViaWebUSB() {
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
console.log('usb ok', globalThis.navigator.usb);
@ -216,50 +290,79 @@ 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;
}
// await push('/sdcard/coffeevending/enable_adb_block_watch', '1');
const stream = await inst.transport.connect(env.PUBLIC_BREW_CONN_PORT);
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
console.log('checking on writer ', writer);
adbWriter.set(writer);
if (writer) {
addNotification('INFO:Enable Brewing Mode T on machine');
} else {
addNotification('WARN:Brewing Mode T unavailable');
setTimeout(async () => {
console.log('reconnecting android server');
await connectToAndroidServer();
}, 5000);
}
(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);
addNotification('WARN:Brewing Mode T Offline ...');
async function connectToAndroidServer(maxRetries = 5) {
let lastError: any;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
let inst = getAdbInstance();
if (!inst) {
console.warn('adb instance not found');
return;
}
})();
} catch (err) {
console.error('Connection failed. Suspect java running or not', err);
addNotification('ERR:Fail to enable brewing mode T');
// 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}`,
3,
500
);
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
console.log('checking on writer ', writer);
adbWriter.set(writer);
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));
}
} catch (e) {
console.error('read error', e);
if (isRecoverableError(e)) {
throw e;
}
throw e;
} finally {
adbWriter.set(null);
addNotification('WARN:Brewing Mode T Offline ...');
}
} else {
addNotification('WARN:Brewing Mode T unavailable');
if (attempt < maxRetries - 1) {
const delay = Math.min(500 * Math.pow(2, attempt) + Math.random() * 500, 5000);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
} else {
throw new Error('Brewing Mode T unavailable after all retries');
}
}
} catch (err) {
lastError = err;
if (attempt == maxRetries - 1) {
break;
}
if (!isRecoverableError(err)) {
break;
}
const delay = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 1000, 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
if (lastError) {
console.error('Connection failed. Suspect java running or not', lastError);
addNotification(`ERR:Fail to enable brewing mode T\n${lastError.message ?? ''}`);
}
}

View file

@ -3,6 +3,7 @@ import { addNotification, notiStore } from '../stores/noti';
import {
currentRecipeVersionsSelector,
materialFromServerQuery,
priceRecipeData,
recipeData,
recipeDataError,
recipeLoading,
@ -16,6 +17,9 @@ import { auth } from '../client/firebase';
import { type RecipeVersion } from '$lib/models/recipe_version.model';
import { goto } from '$app/navigation';
import { socketAlreadySendHeartbeat, socketConnectionOfflineCount } from '../stores/websocketStore';
import type { RecipePrice } from '$lib/models/price.model';
import { sendMessage } from './ws_messageSender';
import { auth as authStore } from '../stores/auth';
export const messages = writable<string[]>([]);
@ -111,6 +115,32 @@ const handlers: Record<string, (payload: any) => void> = {
// console.log('ending stream');
buildOverviewFromServer();
let current_meta = get(recipeStreamMeta);
let curr_user = get(authStore);
let user_info: any;
if (curr_user) {
user_info = {
displayName: curr_user.displayName,
email: curr_user.email,
uid: curr_user.uid
};
}
// send next chain message
sendMessage({
type: 'price',
payload: {
action: {
View: 'sa=all'
},
country: current_meta?.country ?? '',
parameters: '',
user_info
}
});
},
stream_data_extra: (p) => {
// extended data from server, may be extra infos
@ -214,7 +244,16 @@ const handlers: Record<string, (payload: any) => void> = {
let status = p.status;
let to = p.to;
let content = p.content ?? [];
let content: RecipePrice[] = p.content ?? [];
console.log('get price length: ', content.length);
let current_price = get(priceRecipeData);
for (const c of content) {
current_price[c.ProductCode] = c.NewPrice + (c.StringParam ? `,${c.StringParam}` : '');
}
priceRecipeData.set(current_price);
},
heartbeat: (p) => {
socketConnectionOfflineCount.set(0);

View file

@ -9,6 +9,21 @@ function updateMachineStatus(new_status: string) {
current.status = new_status;
machineInfoStore.set(current);
} else {
machineInfoStore.set({
boxId: '',
versions: {
firmware: '',
brew: '',
xmlengine: '',
netcore: '',
devbox: ''
},
devMode: true,
country: '',
status: new_status,
errors: []
});
}
}

View file

@ -2,6 +2,7 @@ import { writable } from 'svelte/store';
import type { RecipeOverview } from '../../../routes/(authed)/recipe/overview/columns';
import type { Material } from '$lib/models/material.model';
import type { RecipeVersion } from '$lib/models/recipe_version.model';
import type { RecipePrice } from '$lib/models/price.model';
export const recipeData = writable<any>(null);
export const recipeLoading = writable(false);
@ -20,6 +21,8 @@ export const currentRecipeVersionsSelector = writable<RecipeVersion[]>([]);
// from server
export const recipeOverviewData = writable<RecipeOverview[] | null>(null);
export const materialData = writable<Material | undefined>();
// price from recipe repo
export const priceRecipeData = writable<{ [key: string]: any }>({});
// machine recipe
export const recipeFromMachine = writable<any>(null);

View file

@ -66,5 +66,6 @@ export type OutMessage =
country: string;
parameters?: string;
override_file?: string;
user_info: any;
};
};

View file

@ -0,0 +1,8 @@
export interface RecipePrice {
ProductCode: string;
NewPrice: number;
StringParam: string | undefined;
Discount: number | undefined;
Percent: number | undefined;
roundup: boolean | undefined;
}