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