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, currentEditingRecipeProductCode,
latestRecipeToppingData, latestRecipeToppingData,
materialFromMachineQuery, materialFromMachineQuery,
materialFromServerQuery materialFromServerQuery,
priceRecipeData
} from '$lib/core/stores/recipeStore'; } from '$lib/core/stores/recipeStore';
import { generateIcing } from '$lib/helpers/icingGen'; import { generateIcing } from '$lib/helpers/icingGen';
import { getMachineStatus, machineInfoStore } from '$lib/core/stores/machineInfoStore'; import { getMachineStatus, machineInfoStore } from '$lib/core/stores/machineInfoStore';
@ -28,6 +29,7 @@
import { sendCommand, sendReset } from '$lib/core/brew/command'; import { sendCommand, sendReset } from '$lib/core/brew/command';
import { isAdbWriterAvailable } from '$lib/core/stores/adbWriter'; import { isAdbWriterAvailable } from '$lib/core/stores/adbWriter';
import { sendToAndroid } from '$lib/core/stores/adbWriter'; import { sendToAndroid } from '$lib/core/stores/adbWriter';
import { departmentStore } from '$lib/core/stores/departments';
// //
let { let {
@ -38,6 +40,8 @@
}: { recipeData: any; onPendingChange: any; productCode: string; refPage: string } = $props(); }: { recipeData: any; onPendingChange: any; productCode: string; refPage: string } = $props();
let menuName: string = $state(''); let menuName: string = $state('');
let menuCurrentPrice: number = $state(0);
let isMenuHideByPrice: boolean = $state(false);
let materialSnapshot: any = $state(); let materialSnapshot: any = $state();
let machineInfoSnapshot: any = $state(); let machineInfoSnapshot: any = $state();
@ -197,6 +201,35 @@
currentEditingRecipeProductCode.set(productCode); 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\ // save old value\
} }
}); });
@ -239,7 +272,16 @@
<Input id="tabs-menu-other-name" value={recipeData['otherName'] ?? ''} /> <Input id="tabs-menu-other-name" value={recipeData['otherName'] ?? ''} />
</div> </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.Content>
</Card.Root> </Card.Root>
</Tabs.Content> </Tabs.Content>

View file

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

View file

@ -14,9 +14,83 @@ import { handleAdbPayload } from '../handlers/adbPayloadHandler';
import { adbWriter } from '../stores/adbWriter'; import { adbWriter } from '../stores/adbWriter';
import { WritableStream } from '@yume-chan/stream-extra'; import { WritableStream } from '@yume-chan/stream-extra';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import type Dice_2 from '@lucide/svelte/icons/dice-2';
let syncConnection: any = null; 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() { export async function connnectViaWebUSB() {
const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice(); const device = await AdbDaemonWebUsbDeviceManager.BROWSER?.requestDevice();
console.log('usb ok', globalThis.navigator.usb); 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 // NOTE: adb reverse is not work by unavailable features support
async function connectToAndroidServer() { async function connectToAndroidServer(maxRetries = 5) {
try { let lastError: any;
let inst = getAdbInstance(); for (let attempt = 0; attempt < maxRetries; attempt++) {
if (!inst) { try {
console.warn('adb instance not found'); let inst = getAdbInstance();
return; 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 ...');
} }
})();
} catch (err) { // add retry mechanism
console.error('Connection failed. Suspect java running or not', err); const stream = await connectWithRetry(
addNotification('ERR:Fail to enable brewing mode T'); 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 { import {
currentRecipeVersionsSelector, currentRecipeVersionsSelector,
materialFromServerQuery, materialFromServerQuery,
priceRecipeData,
recipeData, recipeData,
recipeDataError, recipeDataError,
recipeLoading, recipeLoading,
@ -16,6 +17,9 @@ import { auth } from '../client/firebase';
import { type RecipeVersion } from '$lib/models/recipe_version.model'; import { type RecipeVersion } from '$lib/models/recipe_version.model';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { socketAlreadySendHeartbeat, socketConnectionOfflineCount } from '../stores/websocketStore'; 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[]>([]); export const messages = writable<string[]>([]);
@ -111,6 +115,32 @@ const handlers: Record<string, (payload: any) => void> = {
// console.log('ending stream'); // console.log('ending stream');
buildOverviewFromServer(); 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) => { stream_data_extra: (p) => {
// extended data from server, may be extra infos // extended data from server, may be extra infos
@ -214,7 +244,16 @@ const handlers: Record<string, (payload: any) => void> = {
let status = p.status; let status = p.status;
let to = p.to; 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) => { heartbeat: (p) => {
socketConnectionOfflineCount.set(0); socketConnectionOfflineCount.set(0);

View file

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

View file

@ -66,5 +66,6 @@ export type OutMessage =
country: string; country: string;
parameters?: string; parameters?: string;
override_file?: 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;
}