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:
parent
60424ebe5a
commit
3b70cc9fe8
8 changed files with 269 additions and 48 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ?? ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -66,5 +66,6 @@ export type OutMessage =
|
|||
country: string;
|
||||
parameters?: string;
|
||||
override_file?: string;
|
||||
user_info: any;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
8
src/lib/models/price.model.ts
Normal file
8
src/lib/models/price.model.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface RecipePrice {
|
||||
ProductCode: string;
|
||||
NewPrice: number;
|
||||
StringParam: string | undefined;
|
||||
Discount: number | undefined;
|
||||
Percent: number | undefined;
|
||||
roundup: boolean | undefined;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue