/** * 拍照接口:与拍摄页一致,先写入货号,再走左脚 / 右脚按钮。 * POST /take_photo 由 generateServer 转发到这里。 */ import client from '@/stores/modules/client' import socket from '@/stores/modules/socket' import icpList from '@/utils/ipc' export type FootSide = 'left' | 'right' export type TakePhotoParams = { goods_art_no: string foot: FootSide action: string } type TriggerResult = { started: boolean; code: number; msg?: string } type Trigger = (params: TakePhotoParams) => Promise type FailHandler = (msg: string) => void let trigger: Trigger | null = null let onFail: FailHandler | null = null let shooting = false const TAKE_PHOTO_TIMEOUT = 10 * 60 * 1000 const MCU_ERRORS = ['mcu设备未连接', 'mcu设备未初始化', '当前有未完成的任务'] const LINK_ERRORS = [ '相机未连接', '请检查相机', '请先连接相机', '相机是否连接', 'MCU 连接失败', 'MCU连接失败', 'MCU串口未连接', 'mcu设备未连接', 'MCU硬件连接失败', ] type IpcLike = { on: (channel: string, listener: (...args: any[]) => void) => void removeListener?: (channel: string, listener: (...args: any[]) => void) => void off?: (channel: string, listener: (...args: any[]) => void) => void removeAllListeners?: (channel?: string) => void __takePhotoOriginalRemoveAll?: (channel?: string) => void } const keptListeners = new Map void>>() export function registerTakePhotoTrigger(fn: Trigger, hooks?: { onFail?: FailHandler }) { trigger = fn onFail = hooks?.onFail || null return () => { if (trigger === fn) { trigger = null onFail = null } } } export function parseTakePhotoBody(payload: any): { ok: true; params: TakePhotoParams } | { ok: false; msg: string } { const goodsArtNo = String(payload?.goods_art_no ?? payload?.goodsArtNo ?? payload?.['货号'] ?? '').trim() const foot = normalizeFoot(payload?.foot ?? payload?.side ?? payload?.['左右脚'] ?? payload?.type) if (!goodsArtNo) return { ok: false, msg: '货号不能为空' } if (!foot) return { ok: false, msg: '左右脚不能为空,请传 left/right 或 左/右' } return { ok: true, params: { goods_art_no: goodsArtNo, foot, action: foot === 'right' ? '执行右脚程序' : '执行左脚程序', }, } } function normalizeFoot(raw: unknown): FootSide | null { const text = String(raw ?? '').trim() if (!text) return null const value = text.toLowerCase() if (value === 'right' || value === 'r' || value === '1' || text.includes('右')) return 'right' if (value === 'left' || value === 'l' || value === '0' || text.includes('左')) return 'left' return null } export async function requestTakePhoto(payload: any) { const parsed = parseTakePhotoBody(payload) if (!parsed.ok) return { code: 1, msg: parsed.msg } if (shooting) return { code: 1, msg: '拍摄程序正在运行,请稍候' } const clientStore = client() if (!clientStore.isClient || !clientStore.ipc) { return { code: 1, msg: '拍照接口仅在客户端内可用' } } shooting = true const pending = waitForShoot(clientStore.ipc) try { const started = trigger ? await trigger(parsed.params) : await sendRunMcu(parsed.params) if (pending.settled()) { const result = await pending.promise return withData(parsed.params, result) } if (!started.started) { pending.cancel() return { code: started.code || 1, msg: started.msg || '未能开始拍摄' } } const result = await pending.promise return withData(parsed.params, result) } catch (error: any) { pending.cancel() return { code: 1, msg: error?.message || '拍照失败' } } finally { shooting = false } } function withData(params: TakePhotoParams, result: { code: number; msg: string }) { return { code: result.code, msg: result.msg, data: { goods_art_no: params.goods_art_no, foot: params.foot, action: params.action, }, } } /** 拍摄页未打开时,仍下发与左右脚按钮相同的 run_mcu。 */ async function sendRunMcu(params: TakePhotoParams): Promise { const socketStore = socket() await Promise.race([ socketStore.connectSocket(), new Promise((_, reject) => { setTimeout(() => reject(new Error('设备服务未连接')), 8000) }), ]) socketStore.sendMessage({ type: 'run_mcu', data: { action: params.action, goods_art_no: params.goods_art_no, }, }) return { started: true, code: 0 } } function waitForShoot(ipc: IpcLike) { let settled = false let stepError = '' let stopped = false let resolveFn: (value: { code: number; msg: string }) => void = () => {} const promise = new Promise<{ code: number; msg: string }>((resolve) => { resolveFn = resolve }) const channels = { runMcu: icpList.socket.message + '_run_mcu', mcu: icpList.socket.message + '_mcu', photoTake: icpList.socket.message + '_photo_take', imageProcess: icpList.socket.message + '_image_process', finish: icpList.socket.message + '_photo_take_finish', stop: icpList.socket.message + '_run_mcu_stop', } const handlers: Record void> = {} let timer: ReturnType let release = () => {} const done = (result: { code: number; msg: string }, reset = false) => { if (settled) return settled = true clearTimeout(timer) release() if (reset) onFail?.(result.msg) resolveFn(result) } const failIfLinkLost = (result: any) => { const msg = linkErrorMessage(result) if (!msg) return false done({ code: 1, msg }, true) return true } handlers[channels.runMcu] = (_event, result) => { if (failIfLinkLost(result)) return if (result && result.code !== 0) { done({ code: 1, msg: result.msg || '拍照失败' }) } } handlers[channels.mcu] = (_event, result) => { if (failIfLinkLost(result)) return const msg = String(result?.msg || '') if (result?.code === 1 && MCU_ERRORS.some((item) => msg.includes(item))) { done({ code: 1, msg }, true) } } handlers[channels.photoTake] = (_event, result) => { if (failIfLinkLost(result)) return if (result?.code !== 0 && result?.msg) stepError = result.msg } handlers[channels.imageProcess] = (_event, result) => { failIfLinkLost(result) } handlers[channels.stop] = (_event, result) => { stopped = true stepError = result?.msg || '拍摄已终止' } handlers[channels.finish] = (_event, result) => { if (stopped || stepError) { done({ code: 1, msg: stepError || '拍摄失败' }) return } if (result?.code === 0) { done({ code: 0, msg: result.msg || '拍摄完成' }) return } done({ code: 1, msg: result?.msg || '拍照失败' }, true) } timer = setTimeout(() => { done({ code: 1, msg: '拍照超时' }, true) }, TAKE_PHOTO_TIMEOUT) release = () => { Object.entries(handlers).forEach(([channel, handler]) => keepOff(ipc, channel, handler)) } ensureKeep(ipc) Object.entries(handlers).forEach(([channel, handler]) => keepOn(ipc, channel, handler)) return { promise, settled: () => settled, cancel: () => { if (settled) return settled = true clearTimeout(timer) release() }, } } function linkErrorMessage(result: any): string | null { const msg = String(result?.msg || '') if (!msg) return null if (LINK_ERRORS.some((item) => msg.includes(item))) return msg const status = result?.status ?? result?.device_status if (status === -1 && (msg.includes('相机') || msg.includes('MCU') || msg.includes('mcu'))) return msg return null } function keepOn(ipc: IpcLike, channel: string, handler: (...args: any[]) => void) { let set = keptListeners.get(channel) if (!set) { set = new Set() keptListeners.set(channel, set) } set.add(handler) ipc.on(channel, handler) } function keepOff(ipc: IpcLike, channel: string, handler: (...args: any[]) => void) { const set = keptListeners.get(channel) set?.delete(handler) if (set && set.size === 0) keptListeners.delete(channel) if (typeof ipc.removeListener === 'function') { ipc.removeListener(channel, handler) } else if (typeof ipc.off === 'function') { ipc.off(channel, handler) } } /** 拍摄页卸载时会 removeAllListeners,进行中的接口监听需要挂回去。 */ function ensureKeep(ipc: IpcLike) { if (!ipc || ipc.__takePhotoOriginalRemoveAll || typeof ipc.removeAllListeners !== 'function') return const original = ipc.removeAllListeners.bind(ipc) ipc.__takePhotoOriginalRemoveAll = original ipc.removeAllListeners = (channel?: string) => { const result = original(channel) const channels = channel ? [channel] : Array.from(keptListeners.keys()) channels.forEach((item) => { keptListeners.get(item)?.forEach((handler) => ipc.on(item, handler)) }) return result } }