|
|
@@ -0,0 +1,254 @@
|
|
|
+/**
|
|
|
+ * 拍照接口:与拍摄页一致,先写入货号,再走左脚 / 右脚按钮。
|
|
|
+ * 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<TriggerResult>
|
|
|
+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设备未初始化', '当前有未完成的任务']
|
|
|
+
|
|
|
+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<string, Set<(...args: any[]) => 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<TriggerResult> {
|
|
|
+ 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',
|
|
|
+ finish: icpList.socket.message + '_photo_take_finish',
|
|
|
+ stop: icpList.socket.message + '_run_mcu_stop',
|
|
|
+ }
|
|
|
+ const handlers: Record<string, (...args: any[]) => void> = {}
|
|
|
+ let timer: ReturnType<typeof setTimeout>
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+
|
|
|
+ handlers[channels.runMcu] = (_event, result) => {
|
|
|
+ if (result && result.code !== 0) {
|
|
|
+ done({ code: 1, msg: result.msg || '拍照失败' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ handlers[channels.mcu] = (_event, result) => {
|
|
|
+ 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 (result?.code !== 0 && result?.msg) stepError = result.msg
|
|
|
+ }
|
|
|
+ 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 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
|
|
|
+ }
|
|
|
+}
|