From dbb5ce466c024cebcb8806b2875a317a6edf4aa8 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 4 Mar 2026 13:28:14 +0700 Subject: [PATCH] feat: showing recipe list values - add displaying for values in recipe list, with some editable fields except topping, string params, feed pattern/level Signed-off-by: pakintada@gmail.com --- src/lib/components/dashboard-quick-adb.svelte | 4 +- src/lib/components/recipe-details/columns.ts | 24 +- .../recipe-details/recipe-detail.svelte | 28 +- .../recipelist-mat-select.svelte | 6 +- .../recipe-details/recipelist-table.svelte | 2 +- .../recipe-details/recipelist-value.svelte | 358 +++++++++++++++++- .../components/recipe-editor-dialog.svelte | 6 +- src/lib/core/handlers/messageHandler.ts | 38 +- src/lib/core/handlers/ws_messageSender.ts | 2 +- src/lib/core/stores/recipeStore.ts | 14 +- src/lib/data/recipeService.ts | 135 ++++++- src/routes/(authed)/+layout.svelte | 46 ++- src/routes/(authed)/tools/brew/+page.svelte | 8 +- src/routes/+layout.svelte | 6 + 14 files changed, 640 insertions(+), 37 deletions(-) diff --git a/src/lib/components/dashboard-quick-adb.svelte b/src/lib/components/dashboard-quick-adb.svelte index b4ba09d..15b3c33 100644 --- a/src/lib/components/dashboard-quick-adb.svelte +++ b/src/lib/components/dashboard-quick-adb.svelte @@ -244,7 +244,7 @@ connectionButtonVariant = 'default'; connectDeviceOk = false; } - connectionButtonDisable = false; + // connectionButtonDisable = false; } async function checkStoredCredentials() { @@ -336,7 +336,7 @@ onMount(async () => { await checkStoredCredentials(); - await tryAutoConnect(); + if (!connectDeviceOk && !adb.getAdbInstance()) await tryAutoConnect(); }); diff --git a/src/lib/components/recipe-details/columns.ts b/src/lib/components/recipe-details/columns.ts index 6198ac1..8a0b59a 100644 --- a/src/lib/components/recipe-details/columns.ts +++ b/src/lib/components/recipe-details/columns.ts @@ -21,6 +21,7 @@ import RecipelistValue from './recipelist-value.svelte'; import { DragHandle } from './recipelist-table.svelte'; import { createRawSnippet } from 'svelte'; import RecipelistMatSelect from './recipelist-mat-select.svelte'; +import { recipeDataEvent } from '$lib/core/stores/recipeStore'; export type RecipelistMaterial = { id: number; @@ -29,6 +30,7 @@ export type RecipelistMaterial = { values: { string_param: string; mix_order: number; + stir_time: number; feed: { pattern: number; parameter: number; @@ -69,12 +71,6 @@ export const columns: ColumnDef[] = [ { accessorKey: 'is_use', id: 'is_use', - header: ({ column }) => - renderSnippet( - createRawSnippet(() => ({ - render: () => '
Enable
' - })) - ), cell: ({ row }) => { return renderComponent(RecipelistIsuse, { checked: row.original.is_use, @@ -96,6 +92,11 @@ export const columns: ColumnDef[] = [ onMatChange: (value: any) => { row.original.material_id = value; console.log('change mat', value); + recipeDataEvent.set({ + event_type: 'mat_change', + payload: value, + index: row.original.id + }); row.toggleSelected(row.original.is_use); } }); @@ -107,6 +108,17 @@ export const columns: ColumnDef[] = [ header: ({ column }) => 'Values', cell: ({ row }) => { return renderComponent(RecipelistValue, { + row_uid: row.original.id, + mat_id: row.original.material_id, + onEditValue: (changes: any) => { + // get change parameters + }, + onDetectMixOrder: () => { + // set next + }, + onDetectToppingSlot: () => { + // replace display mat with topping name + }, ...row.original.values }); } diff --git a/src/lib/components/recipe-details/recipe-detail.svelte b/src/lib/components/recipe-details/recipe-detail.svelte index 3509a51..dfd93af 100644 --- a/src/lib/components/recipe-details/recipe-detail.svelte +++ b/src/lib/components/recipe-details/recipe-detail.svelte @@ -8,7 +8,11 @@ import { columns, type RecipelistMaterial } from './columns'; import { get, readable, writable } from 'svelte/store'; - import { materialFromMachineQuery, materialFromServerQuery } from '$lib/core/stores/recipeStore'; + import { + latestRecipeToppingData, + materialFromMachineQuery, + materialFromServerQuery + } from '$lib/core/stores/recipeStore'; import { generateIcing } from '$lib/helpers/icingGen'; import { machineInfoStore } from '$lib/core/stores/machineInfoStore'; import MachineInfo from '../machine-info.svelte'; @@ -29,18 +33,17 @@ let recipeListMatState: RecipelistMaterial[] = $state([]); let recipeListOriginal: RecipelistMaterial[] = $state([]); + let toppingSlotState: any = $state([]); + function remappingToColumn(data: any[]): RecipelistMaterial[] { let ret: RecipelistMaterial[] = []; // expect recipelist if (materialSnapshot) { let d_cnt = 0; for (let rpl of data) { - let mat = - refPage == 'brew' - ? materialSnapshot.filter( - (x: any) => x['id'].toString() === rpl['materialPathId'].toString() - )[0] - : materialSnapshot[rpl['materialPathId']]; + let mat = materialSnapshot.filter( + (x: any) => x['id'].toString() === rpl['materialPathId'].toString() + )[0]; // console.log('mat filter get', Object(mat), Object.keys(mat)); @@ -61,9 +64,10 @@ values: { string_param: rpl['StringParam'], mix_order: rpl['MixOrder'], + stir_time: rpl['stirTime'], feed: { - pattern: rpl['feedPattern'], - parameter: rpl['feedParameter'] + pattern: rpl['FeedPattern'], + parameter: rpl['FeedParameter'] }, powder: { gram: rpl['powderGram'], @@ -113,9 +117,11 @@ materialSnapshot = get(materialFromMachineQuery); } - console.log(`detail : ${JSON.stringify(recipeData)}`); - recipeListMatState = remappingToColumn(recipeData['recipes']); + toppingSlotState = recipeData['ToppingSet']; + + latestRecipeToppingData.set(toppingSlotState); + // save old value\ } }); diff --git a/src/lib/components/recipe-details/recipelist-mat-select.svelte b/src/lib/components/recipe-details/recipelist-mat-select.svelte index 3e5acf4..012d522 100644 --- a/src/lib/components/recipe-details/recipelist-mat-select.svelte +++ b/src/lib/components/recipe-details/recipelist-mat-select.svelte @@ -4,7 +4,7 @@ import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index'; import { get } from 'svelte/store'; import { - materialData, + materialFromServerQuery, materialFromMachineQuery, referenceFromPage } from '$lib/core/stores/recipeStore'; @@ -26,7 +26,9 @@ onMount(() => { refPage = get(referenceFromPage); if (refPage === 'brew') allMatData = get(materialFromMachineQuery); - else if (refPage === 'overview') allMatData = get(materialData); + else if (refPage === 'overview') allMatData = get(materialFromServerQuery); + + // console.log('all mat data', JSON.stringify(allMatData)); }); diff --git a/src/lib/components/recipe-details/recipelist-table.svelte b/src/lib/components/recipe-details/recipelist-table.svelte index 3d1ef58..ad6445a 100644 --- a/src/lib/components/recipe-details/recipelist-table.svelte +++ b/src/lib/components/recipe-details/recipelist-table.svelte @@ -105,7 +105,7 @@ Recipe List - Material used in this menu's brewing process + Material used in this menu's brewing process. diff --git a/src/lib/components/recipe-details/recipelist-value.svelte b/src/lib/components/recipe-details/recipelist-value.svelte index 6957dd4..ef6a7f4 100644 --- a/src/lib/components/recipe-details/recipelist-value.svelte +++ b/src/lib/components/recipe-details/recipelist-value.svelte @@ -1,6 +1,360 @@ + +{#if currentMaterialType === 'topping'} + +
+ +
+ + + + +
+
+{:else} +
+
+ + {#if !hasMixOrder} + + + {#if currentStringParams['esp-v2-press-value']} +
+ +
+ + triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)} + /> +

mA

+
+
+ {/if} + + {#if water.yield > 0} +
+ +
+ + triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)} + /> +

ml

+
+
+ {/if} + + {#if water.cold > 0} +
+ +
+ + triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)} + /> +

ml

+
+
+ {/if} + + {#if currentMaterialType !== 'cup' && currentMaterialType !== 'topping' && stir_time > 0} +
+ +
+ + triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)} + /> +

sec

+
+
+ {/if} + {/if} + + + {#if currentMaterialType === 'syrup' || currentMaterialType === 'powder' || currentMaterialType === 'bean'} +
+ +
+ + triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)} + /> +

gram

+
+
+ {/if} +
+ + {#if feed.parameter > 0 || feed.pattern > 0} +
+ + +
+ {/if} +
+{/if} diff --git a/src/lib/components/recipe-editor-dialog.svelte b/src/lib/components/recipe-editor-dialog.svelte index 7c4e326..ff5b982 100644 --- a/src/lib/components/recipe-editor-dialog.svelte +++ b/src/lib/components/recipe-editor-dialog.svelte @@ -76,7 +76,7 @@ let recipe01Snap = recipeServerSnapshot['recipe']; if (recipe01Snap) { currentData = recipe01Snap[productCode] ?? {}; - console.log(`current data : ${JSON.stringify(Object.keys(recipe01Snap))}`); + // console.log(`current data : ${JSON.stringify(Object.keys(recipe01Snap))}`); if (currentData.MenuStatus) { currentMenuStatus = matchMenuStatus(currentData.MenuStatus); } @@ -87,7 +87,9 @@ {#if isDesktop.current} - e.preventDefault()}>View + e.preventDefault()} + >View Edit Recipe {productCode} diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts index 45d7366..db1846a 100644 --- a/src/lib/core/handlers/messageHandler.ts +++ b/src/lib/core/handlers/messageHandler.ts @@ -6,7 +6,9 @@ import { recipeDataError, recipeLoading, recipeOverviewData, - recipeStreamMeta + recipeStreamMeta, + toppingGroupFromServerQuery, + toppingListFromServerQuery } from '../stores/recipeStore'; import { buildOverviewFromServer } from '$lib/data/recipeService'; import { auth } from '../client/firebase'; @@ -103,18 +105,48 @@ const handlers: Record void> = { // know type switch (extp) { case 'matset': - let curr_mat_query = get(materialFromServerQuery); + let curr_mat_query = get(materialFromServerQuery) ?? []; + + if (!curr_mat_query) { + curr_mat_query = []; + } + // ex_payload has chunks of material setting for (let m of ex_payload) { let mid = m.id; - curr_mat_query[mid] = m; + // curr_mat_query[mid] = m; + + curr_mat_query.push(m); } + // console.log('current materials: ', JSON.stringify(curr_mat_query)); materialFromServerQuery.set(curr_mat_query); break; case 'topplist': + let curr_topping_list_query = get(toppingListFromServerQuery) ?? []; + if (!curr_topping_list_query) { + curr_topping_list_query = []; + } + + for (let t of ex_payload) { + curr_topping_list_query.push(t); + } + + toppingListFromServerQuery.set(curr_topping_list_query); + break; case 'toppgrp': + let curr_topping_group_query = get(toppingGroupFromServerQuery) ?? []; + if (!curr_topping_group_query) { + curr_topping_group_query = []; + } + + for (let t of ex_payload) { + curr_topping_group_query.push(t); + } + + toppingGroupFromServerQuery.set(curr_topping_group_query); + break; } } diff --git a/src/lib/core/handlers/ws_messageSender.ts b/src/lib/core/handlers/ws_messageSender.ts index ddc60b1..ce86af6 100644 --- a/src/lib/core/handlers/ws_messageSender.ts +++ b/src/lib/core/handlers/ws_messageSender.ts @@ -16,7 +16,7 @@ export function sendMessage(msg: OutMessage): boolean { currentQueue.push(data); queue.set(currentQueue); - addNotification('WARN:Queuing overview view request'); + addNotification('WARN:Queuing request'); return false; } diff --git a/src/lib/core/stores/recipeStore.ts b/src/lib/core/stores/recipeStore.ts index 91ca57e..b61cdce 100644 --- a/src/lib/core/stores/recipeStore.ts +++ b/src/lib/core/stores/recipeStore.ts @@ -22,7 +22,19 @@ export const recipeFromMachineLoading = writable(false); export const recipeFromMachineError = writable(null); export const recipeFromServerQuery = writable({}); -export const materialFromServerQuery = writable({}); +export const materialFromServerQuery = writable([]); +export const toppingListFromServerQuery = writable([]); +export const toppingGroupFromServerQuery = writable([]); + +// latest recipe data (use for interacting) +export const latestRecipeToppingData = writable([]); + +// edit data update +export const recipeDataEvent = writable<{ + event_type: string; + payload: any; + index: number | undefined; +} | null>(null); // NOTE: must not have any nested structures // { recipe: {}, materials: {}, toppings: { groups: {}, lists: {} } } diff --git a/src/lib/data/recipeService.ts b/src/lib/data/recipeService.ts index 6391194..f5b4984 100644 --- a/src/lib/data/recipeService.ts +++ b/src/lib/data/recipeService.ts @@ -137,4 +137,137 @@ function buildOverviewFromServer() { } } -export { buildOverviewFromServer }; +const rangeMaterialMapping: { [key: string]: (id: number) => boolean } = { + soda: (id: number) => id == 1031, + water: (id: number) => id == 1, + ice: (id: number) => id == 9100, + whipper: (id: number) => id == 8102, + bean: (id: number) => inRange(1001, 1009, id) || inRange(1100, 1199, id), + leaves: (id: number) => inRange(1600, 1799, id), + syrup: (id: number) => + inRange(1032, 1039, id) || inRange(1020, 1030, id) || inRange(1200, 1299, id), + powder: (id: number) => inRange(1040, 1080, id) || inRange(1300, 1399, id), + cup: (id: number) => inRange(9500, 9549, id), + lid: (id: number) => inRange(9600, 9649, id), + straw: (id: number) => inRange(9700, 9749, id), + icecream: (id: number) => inRange(2100, 2200, id), + topping: (id: number) => inRange(8110, 8130, id) +}; + +function inRange(min: number, max: number, value: number) { + // console.log(min, max, value, value >= min && value <= max); + return value >= min && value <= max; +} + +function getCategories() { + let keys = Object.keys(rangeMaterialMapping); + keys.push('equipment'); + return keys; +} + +function getMaterialType(materialId: number) { + for (const key of Object.keys(rangeMaterialMapping)) { + if (rangeMaterialMapping[key](materialId)) { + return key; + } + } + + return 'others'; +} + +function isNonMaterial(materialId: number) { + // test cup, lid, straw + return ( + rangeMaterialMapping['cup'](materialId) || + rangeMaterialMapping['lid'](materialId) || + rangeMaterialMapping['straw'](materialId) || + rangeMaterialMapping['whipper'](materialId) || + rangeMaterialMapping['ice'](materialId) + ); +} + +const MATERIAL_INTER_GUARD = 300000; + +// Inter mode checker +function convertFromInterProductCode(materialId: number) { + if (materialId > MATERIAL_INTER_GUARD) { + try { + let pure_id = materialId.toString().slice(2); + return parseInt(pure_id); + } catch (e) {} + } + + return materialId; +} + +// Extract material id from display +function extractMaterialIdFromDisplay(material_id: string): number { + // console.log('extracting from ', material_id); + let mat_split = material_id.split(' '); + let pure_mat_id = mat_split[mat_split.length - 1] ?? '0'; + + try { + // console.log('pure mat', pure_mat_id); + let mat_id_int = parseInt(pure_mat_id.replace('(', '').replace(')', '').trim()); + return mat_id_int; + } catch (ignored) {} + return -1; +} + +// StringParam + +class StringParam { + StringParam: string; + extractedParams: { [key: string]: any } = {}; + + constructor(StringParam: string) { + this.StringParam = StringParam; + } + + extract() { + // split by , + const params = this.StringParam.split(','); + + for (const param of params) { + const [key, value] = param.split('='); + if (key != '') { + this.extractedParams[key] = value; + } + } + + return this; + } + + as_list() { + let res: { pkey: string; pvalue: any }[] = []; + // iter through param + for (let p of Object.keys(this.extractedParams)) { + res.push({ + pkey: p, + pvalue: this.extractedParams[p] + }); + } + + return res; + } +} + +const stringParamsDefinition: { [key: string]: string } = { + 'esp-v2-press-value': 'Current 100 x mA ( 10 - 24 )' +}; + +const conditionTests: { [key: string]: (arg: any) => boolean } = { + 'not-zero': (arg: any) => arg != 0, + zero: (arg: any) => arg == 0, + 'false-if-another-exist': (arg: any) => arg[1] != undefined +}; + +export { + buildOverviewFromServer, + StringParam, + convertFromInterProductCode, + getMaterialType, + getCategories, + isNonMaterial, + extractMaterialIdFromDisplay +}; diff --git a/src/routes/(authed)/+layout.svelte b/src/routes/(authed)/+layout.svelte index 9ef40a7..f1dee64 100644 --- a/src/routes/(authed)/+layout.svelte +++ b/src/routes/(authed)/+layout.svelte @@ -11,14 +11,56 @@ import { auth } from '$lib/core/stores/auth'; import { get } from 'svelte/store'; import { connectToWebsocket } from '$lib/core/stores/websocketStore'; + import * as adb from '$lib/core/adb/adb'; + import { addNotification } from '$lib/core/stores/noti'; + import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb'; + import AdbWebCredentialStore from '@yume-chan/adb-credential-web'; + import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager'; let { children } = $props(); - onMount(() => { + async function tryAutoConnect() { + try { + if (!('usb' in navigator) || !AdbDaemonWebUsbDeviceManager.BROWSER) { + throw new Error('WebUSB not supported, try using fallback or different browser'); + } + + const devices = await AdbDaemonWebUsbDeviceManager.BROWSER.getDevices(); + if (!devices || devices.length == 0) { + throw new Error('No device found'); + } + + if (devices.length > 1) { + throw new Error('Too many connected devices'); + } + + const device = devices[0]; + const credStore = new AdbWebCredentialStore(); + + try { + await adb.connectDeviceByCred(device, credStore); + return true; + } catch (e: any) { + if (e.message === 'CREDENTIAL_EXPIRED') { + try { + await deviceCredentialManager.clearAllCredentials(); + } catch (ignored) {} + } + + return false; + } + } catch (e) { + console.error('error on auto connect brew page', e); + addNotification('ERROR:Failed to auto connect, please try again'); + } + } + + onMount(async () => { let currentUser = get(auth); - console.log(`on mount layout current user: ${JSON.stringify(currentUser)}`); + // console.log(`on mount layout current user: ${JSON.stringify(currentUser)}`); if (currentUser) { connectToWebsocket(); + await tryAutoConnect(); } }); diff --git a/src/routes/(authed)/tools/brew/+page.svelte b/src/routes/(authed)/tools/brew/+page.svelte index e083d11..026bd72 100644 --- a/src/routes/(authed)/tools/brew/+page.svelte +++ b/src/routes/(authed)/tools/brew/+page.svelte @@ -21,6 +21,7 @@ import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb'; import AdbWebCredentialStore from '@yume-chan/adb-credential-web'; import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager'; + import { afterNavigate } from '$app/navigation'; const sourceDir = '/sdcard/coffeevending'; @@ -36,8 +37,11 @@ let instance = adb.getAdbInstance(); recipeFromMachineLoading.set(true); referenceFromPage.set('brew'); + console.log('check instance', instance); if (instance) { + console.log('instance passed!'); let dev_recipe = await adb.pull(`${sourceDir}/cfg/recipe_branch_dev.json`); + // console.log('dev recipe ok', dev_recipe); if (dev_recipe) { if (dev_recipe.length == 0) { // case error, do last retry @@ -158,9 +162,7 @@ } } - onMount(async () => { - // do auto connect - if (!adb.getAdbInstance()) await tryAutoConnect(); + afterNavigate(async () => { await startFetchRecipeFromMachine(); }); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9fef3e7..9694127 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -25,6 +25,7 @@ extractCookieOnNonBrowser, setCookieOnNonBrowser } from '$lib/helpers/cookie'; + import { connectToWebsocket } from '$lib/core/stores/websocketStore'; let { children } = $props(); @@ -48,6 +49,11 @@ }); return authStore.subscribe(async function (user) { // console.log(`store get ${JSON.stringify(user)}`); + + if (user != null) { + connectToWebsocket(); + } + // reloading permissions if (get(currentPermissions).length == 0 && user != null) { // need update