- fix: cannot adb command from blocking suspicious cmd - change: logging to logtape lib - change: adb payload handler format to send payload size as header for exact match payload Signed-off-by: pakintada@gmail.com <Pakin>
432 lines
13 KiB
TypeScript
432 lines
13 KiB
TypeScript
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<Promise<void>>();
|
|
|
|
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'] ?? '<not set>',
|
|
name: rp_from_payload['name']
|
|
? rp_from_payload['name']
|
|
: (rp_from_payload['otherName'] ?? '<not set>'),
|
|
description: rp_from_payload['desciption']
|
|
? rp_from_payload['desciption']
|
|
: (rp_from_payload['otherDescription'] ?? '<not set>'),
|
|
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 ?? '<not set>',
|
|
name: rp.name ? rp.name : (rp.otherName ?? '<not set>'),
|
|
description: rp.description
|
|
? rp.description
|
|
: (rp.otherDescription ?? '<not set>'),
|
|
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 };
|