diff --git a/src/lib/components/recipe-details/recipe-detail.svelte b/src/lib/components/recipe-details/recipe-detail.svelte index bc4115a..b1100ea 100644 --- a/src/lib/components/recipe-details/recipe-detail.svelte +++ b/src/lib/components/recipe-details/recipe-detail.svelte @@ -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 @@ -
+
+ + + +
diff --git a/src/lib/components/recipe-editor-dialog.svelte b/src/lib/components/recipe-editor-dialog.svelte index 753de1e..c80a281 100644 --- a/src/lib/components/recipe-editor-dialog.svelte +++ b/src/lib/components/recipe-editor-dialog.svelte @@ -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} - Working + Working {$machineInfoStore?.status} {/if} diff --git a/src/lib/core/adb/adb.ts b/src/lib/core/adb/adb.ts index b8b1c09..6abe7ea 100644 --- a/src/lib/core/adb/adb.ts +++ b/src/lib/core/adb/adb.ts @@ -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( + connectionFn: () => Promise, + description: string, + maxRetries: number = 5, + baseDelayMs: number = 1000 +): Promise { + let lastError: any; + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const timeoutPromise = new Promise((_, 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 ?? ''}`); } } diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts index 1c6e394..c7e2088 100644 --- a/src/lib/core/handlers/messageHandler.ts +++ b/src/lib/core/handlers/messageHandler.ts @@ -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([]); @@ -111,6 +115,32 @@ const handlers: Record 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 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); diff --git a/src/lib/core/stores/machineInfoStore.ts b/src/lib/core/stores/machineInfoStore.ts index e56b31e..9cf3abf 100644 --- a/src/lib/core/stores/machineInfoStore.ts +++ b/src/lib/core/stores/machineInfoStore.ts @@ -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: [] + }); } } diff --git a/src/lib/core/stores/recipeStore.ts b/src/lib/core/stores/recipeStore.ts index 5c7e606..0a97cc2 100644 --- a/src/lib/core/stores/recipeStore.ts +++ b/src/lib/core/stores/recipeStore.ts @@ -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(null); export const recipeLoading = writable(false); @@ -20,6 +21,8 @@ export const currentRecipeVersionsSelector = writable([]); // from server export const recipeOverviewData = writable(null); export const materialData = writable(); +// price from recipe repo +export const priceRecipeData = writable<{ [key: string]: any }>({}); // machine recipe export const recipeFromMachine = writable(null); diff --git a/src/lib/core/types/outMessage.ts b/src/lib/core/types/outMessage.ts index 2be6686..afe8c23 100644 --- a/src/lib/core/types/outMessage.ts +++ b/src/lib/core/types/outMessage.ts @@ -66,5 +66,6 @@ export type OutMessage = country: string; parameters?: string; override_file?: string; + user_info: any; }; }; diff --git a/src/lib/models/price.model.ts b/src/lib/models/price.model.ts new file mode 100644 index 0000000..f653051 --- /dev/null +++ b/src/lib/models/price.model.ts @@ -0,0 +1,8 @@ +export interface RecipePrice { + ProductCode: string; + NewPrice: number; + StringParam: string | undefined; + Discount: number | undefined; + Percent: number | undefined; + roundup: boolean | undefined; +}