import { logger } from '$lib/core/utils/logger'; import { get } from 'svelte/store'; import { updateMachineStatus } from '../stores/machineInfoStore'; import { addNotification } from '../stores/noti'; import { loadAndroidRecipeExportFromDevice, saveAndroidRecipeExportPayload } from '../services/androidRecipeExportService'; import { handleIncomingMessages } from './messageHandler'; import { setMenuSaved, setMenuSaveError } from '../stores/menuSaveStore'; import { recipeFromMachine, recipeFromMachineQuery } from '../stores/recipeStore'; import { buildTags, getMenuStatus } from '$lib/data/recipeService'; import * as semver from 'semver'; import { env } from '$env/dynamic/public'; import { getContext } from 'svelte'; import { GlobalEventBus, useEventBus } from '../utils/eventBus'; type AdbPayload = { type: string; payload: any }; let queuedPromises = new Array>(); async function handleAdbPayload(raw_payload: string) { // logger.info('[ADB] Received payload:', raw_payload.slice(0, 300)); const APP_VERSION = env.PUBLIC_APP_SEMVER; // const bus = useEventBus(); try { const payload: AdbPayload = JSON.parse(raw_payload); // logger.info('[ADB] Parsed type:', payload.type, 'payload:', payload.payload); // Emit payload event for terminal drawer payload viewer GlobalEventBus.emit('adb:payload', payload); let payload_type = payload.type; let sub_type = null; // sub type handler if (payload_type.includes('/')) { // let tspl = payload_type.split('/'); payload_type = tspl[0]; sub_type = tspl[1]; } switch (payload_type) { case 'log': let log_level = payload.payload['level'] ?? 'INFO'; let log_message = payload.payload['msg'] ?? ''; if (log_message !== '') { logger.info('[ADB LOG]', log_level, log_message); addNotification(`${log_level}:${log_message}`); } break; case 'response': if (typeof payload.payload === 'string' || payload.payload instanceof String) { // single message response let raw_payload = payload.payload.toString(); if ( raw_payload.startsWith('save_recipe_machine') || raw_payload.startsWith('save_recipe_menu_file') ) { let res = raw_payload.split('/'); let pd = res[1] ?? ''; let action = res[2] ?? ''; let uiAction = res[3] ?? ''; // logger.info('[ADB] Save response parsed:', { pd, action, uiAction, raw_payload }); // Track menu save status if (raw_payload.startsWith('save_recipe_menu_file') && pd) { if (action === 'success' || action === 'ok' || uiAction === 'refreshNow') { setMenuSaved(pd); addNotification(`INFO:Menu saved: ${pd}`); } else if (action === 'error' || action === 'fail') { setMenuSaveError(pd, 'Save failed'); addNotification(`ERR:Failed to save menu: ${pd}`); } else { // Assume success if we get a response setMenuSaved(pd); addNotification(`INFO:Menu saved: ${pd}`); } } if (raw_payload.startsWith('save_recipe_machine')) { // handleIncomingMessages( // JSON.stringify({ // type: 'ui_action', // payload: { // action: uiAction, // from: 'brew', // ref: `${pd}.${action}` // } // }) // ); if (pd.length > 0) { setMenuSaved(pd); } } } else if (raw_payload.startsWith('state')) { let res = raw_payload.split('/'); let new_machine_state = res[1] ?? ''; if (new_machine_state != '') { updateMachineStatus(new_machine_state.replace('MACHINE_STATE_', '')); } } } break; case 'ACK': // acknowledge response from app if (payload.payload !== 'OK') { // abnormal logger.error('error from ACK', payload.payload); addNotification('ERR:Request rejected'); } break; case 'machine': // machine state if (payload.payload) { let states = payload.payload.split('/'); let curr = states[0].replace('MACHINE_STATE_', ''); let next = states[1].replace('MACHINE_STATE_', ''); logger.info('current state', curr, 'next state', next); addNotification('INFO:Machine Status Updated, ' + next); updateMachineStatus(next); } break; case 'brew-finish': if (payload.payload) { let plist = payload.payload.split('/'); let pd = plist[0] ?? ''; let total_time = plist[1] ?? ''; let mode_ref = plist[2] ?? ''; // update recipe data store logger.info('brewing finish', pd, 'total time', total_time); // update recipe from brew now let recipeDevSnapshot = get(recipeFromMachineQuery) ?? {}; let recipe01Snap = recipeDevSnapshot['recipe']; if (recipe01Snap) { recipe01Snap[pd].total_time = mode_ref != 'sim' ? total_time : recipe01Snap[pd].total_time; recipeDevSnapshot['recipe'] = recipe01Snap; recipeFromMachineQuery.set(recipeDevSnapshot); } } break; case 'error': // show error to user from brew app addNotification(`ERR:${payload.payload}`); // send message to server if needed break; case 'recipe-export': if (payload.payload?.content) { saveAndroidRecipeExportPayload({ content: payload.payload.content, exportedAt: payload.payload.exportedAt, source: payload.payload.source, fileSizeBytes: payload.payload.fileSizeBytes, lineCount: payload.payload.lineCount, message: payload.payload.message }); addNotification('INFO:Recipe export received from Android'); } else if (payload.payload?.message) { addNotification(`ERR:${payload.payload.message}`); } break; case 'recipe-export-ready': if (payload.payload?.message && payload.payload?.fileSizeBytes === 0) { addNotification(`ERR:${payload.payload.message}`); break; } void loadAndroidRecipeExportFromDevice({ exportedAt: payload.payload?.exportedAt, source: payload.payload?.source, fileSizeBytes: payload.payload?.fileSizeBytes, lineCount: payload.payload?.lineCount, message: payload.payload?.message }) .then(() => addNotification('INFO:Recipe export loaded from Android')) .catch((error: any) => addNotification(`ERR:${error?.message ?? 'Unable to load recipe export from Android'}`) ); break; case 'get-recipe-response': // only update recipe from memory of brew app if (!semver.satisfies(APP_VERSION, '^0.0.3')) { // reject logger.info('unsupported version'); break; } let stream_recipe_idx = -1; try { stream_recipe_idx = parseInt(sub_type ?? '-1'); } catch (_) {} if (stream_recipe_idx > -1) { // TODO: update both recipeFromMachine and recipeFromMachineQuery // queuedPromises.push( new Promise((resolve, reject) => { let recipeRawFromMachine = get(recipeFromMachine); let recipeMachineQ = get(recipeFromMachineQuery); if (!Object.keys(recipeMachineQ).includes('recipe')) { recipeMachineQ = { ...recipeMachineQ, recipe: {} }; } let current_r01_query = recipeMachineQ.recipe; let rp_from_payload = JSON.parse(payload.payload); try { if ( recipeRawFromMachine != null && Object.keys(recipeRawFromMachine).includes('Recipe01') ) { let is_update = false; // update, assume that we already fetch for (let rp of recipeRawFromMachine['Recipe01']) { if (rp['productCode'] == rp_from_payload['productCode']) { rp = rp_from_payload; is_update = true; break; } } // new menu if (!is_update) recipeRawFromMachine['Recipe01'].push(rp_from_payload); } else { // not initialize recipeRawFromMachine = { Recipe01: [rp_from_payload] }; } // build as overview style compatible let overview_menu = { productCode: rp_from_payload['productCode'] ?? '', name: rp_from_payload['name'] ? rp_from_payload['name'] : (rp_from_payload['otherName'] ?? ''), description: rp_from_payload['desciption'] ? rp_from_payload['desciption'] : (rp_from_payload['otherDescription'] ?? ''), tags: buildTags(rp_from_payload), status: getMenuStatus(rp_from_payload['MenuStatus']) }; current_r01_query[overview_menu.productCode] = overview_menu; recipeMachineQ = { ...recipeMachineQ, recipe: current_r01_query }; recipeFromMachineQuery.set(recipeMachineQ); recipeFromMachine.set(recipeRawFromMachine); if (stream_recipe_idx == 0) { addNotification(`INFO:Loading recipes ...`); } resolve(); } catch (e) { reject(e); } }) ); } else if (sub_type == 'end' && payload.payload.includes('finish')) { let force_reload = payload.payload.includes('reload'); logger.info('queued recipes: ', queuedPromises.length); if (queuedPromises.length > 0) { try { await Promise.all(queuedPromises); logger.info('clear all recipe promises'); queuedPromises = new Array(); GlobalEventBus.emitUntilConsumed('recipe-event', { type: 'load-recipe', status: 'end', reload: force_reload }); } catch (e) { logger.error('some promise failed: ', e); GlobalEventBus.emitUntilConsumed('recipe-event', { type: 'load-recipe-fail', status: 'end', reload: force_reload }); } } } else if (sub_type?.startsWith('mat')) { let recipeRawFromMachine = get(recipeFromMachine); if (sub_type == 'mat-0') { addNotification('INFO:Loading materials ...'); } if ( recipeRawFromMachine != null && Object.keys(recipeRawFromMachine).includes('MaterialSetting') ) { recipeRawFromMachine.MaterialSetting.push(JSON.parse(payload.payload)); } else { // not has field material yet recipeRawFromMachine = { ...recipeRawFromMachine, MaterialSetting: [JSON.parse(payload.payload)] }; } // logger.info('checking add mat', recipeRawFromMachine); recipeFromMachine.set(recipeRawFromMachine); } else if (sub_type == 'toppings') { let recipeRawFromMachine = get(recipeFromMachine); let topping_raw = JSON.parse(payload.payload); logger.info('receive topping', topping_raw); // recipeRawFromMachine = { ...recipeRawFromMachine, Topping: topping_raw }; // materialFromMachineQuery addNotification('INFO:Loading toppings ...'); recipeFromMachine.set(recipeRawFromMachine); } else { if (sub_type == null) { try { let big_recipe = JSON.parse(JSON.stringify(payload.payload)); logger.info(`get large recipe keys: ${Object.keys(big_recipe)}`); let recipes = JSON.parse(big_recipe?.recipes); let materials = JSON.parse(big_recipe?.materials); let toppings = JSON.parse(big_recipe?.toppings); let updated = { Recipe01: recipes, MaterialSetting: materials, Topping: toppings }; recipeFromMachine.set(updated); // ===================================== // // Build Overview // let queries = []; for (let rp of recipes) { queries.push({ productCode: rp.productCode ?? '', name: rp.name ? rp.name : (rp.otherName ?? ''), description: rp.description ? rp.description : (rp.otherDescription ?? ''), tags: buildTags(rp), status: getMenuStatus(rp.MenuStatus) }); } let cached_query_from_machine = get(recipeFromMachineQuery); cached_query_from_machine = { ...cached_query_from_machine, recipe: queries }; recipeFromMachineQuery.set(cached_query_from_machine); // ===================================== // // Send finish signal GlobalEventBus.emitUntilConsumed('recipe-event', { type: 'load-recipe', status: 'end', reload: true }); } catch (e) { logger.error('some promise failed: ', e); GlobalEventBus.emitUntilConsumed('recipe-event', { type: 'load-recipe-fail', status: 'end', reload: true }); } } else { logger.info(`unhandled sub type: %${sub_type}%`); } // unhandled sub type } break; default: } } catch (error: any) { // invalid format — emit error event so listeners (e.g. terminal) can react GlobalEventBus.emit('adb:payload-error', { raw_payload, error: error?.message ?? 'Unknown parse error', timestamp: new Date().toISOString() }); } } export { handleAdbPayload };