takePhotoApi.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /**
  2. * 拍照接口:与拍摄页一致,先写入货号,再走左脚 / 右脚按钮。
  3. * POST /take_photo 由 generateServer 转发到这里。
  4. */
  5. import client from '@/stores/modules/client'
  6. import socket from '@/stores/modules/socket'
  7. import icpList from '@/utils/ipc'
  8. export type FootSide = 'left' | 'right'
  9. export type TakePhotoParams = {
  10. goods_art_no: string
  11. foot: FootSide
  12. action: string
  13. }
  14. type TriggerResult = { started: boolean; code: number; msg?: string }
  15. type Trigger = (params: TakePhotoParams) => Promise<TriggerResult>
  16. type FailHandler = (msg: string) => void
  17. let trigger: Trigger | null = null
  18. let onFail: FailHandler | null = null
  19. let shooting = false
  20. const TAKE_PHOTO_TIMEOUT = 10 * 60 * 1000
  21. const MCU_ERRORS = ['mcu设备未连接', 'mcu设备未初始化', '当前有未完成的任务']
  22. const LINK_ERRORS = [
  23. '相机未连接',
  24. '请检查相机',
  25. '请先连接相机',
  26. '相机是否连接',
  27. 'MCU 连接失败',
  28. 'MCU连接失败',
  29. 'MCU串口未连接',
  30. 'mcu设备未连接',
  31. 'MCU硬件连接失败',
  32. ]
  33. type IpcLike = {
  34. on: (channel: string, listener: (...args: any[]) => void) => void
  35. removeListener?: (channel: string, listener: (...args: any[]) => void) => void
  36. off?: (channel: string, listener: (...args: any[]) => void) => void
  37. removeAllListeners?: (channel?: string) => void
  38. __takePhotoOriginalRemoveAll?: (channel?: string) => void
  39. }
  40. const keptListeners = new Map<string, Set<(...args: any[]) => void>>()
  41. export function registerTakePhotoTrigger(fn: Trigger, hooks?: { onFail?: FailHandler }) {
  42. trigger = fn
  43. onFail = hooks?.onFail || null
  44. return () => {
  45. if (trigger === fn) {
  46. trigger = null
  47. onFail = null
  48. }
  49. }
  50. }
  51. export function parseTakePhotoBody(payload: any): { ok: true; params: TakePhotoParams } | { ok: false; msg: string } {
  52. const goodsArtNo = String(payload?.goods_art_no ?? payload?.goodsArtNo ?? payload?.['货号'] ?? '').trim()
  53. const foot = normalizeFoot(payload?.foot ?? payload?.side ?? payload?.['左右脚'] ?? payload?.type)
  54. if (!goodsArtNo) return { ok: false, msg: '货号不能为空' }
  55. if (!foot) return { ok: false, msg: '左右脚不能为空,请传 left/right 或 左/右' }
  56. return {
  57. ok: true,
  58. params: {
  59. goods_art_no: goodsArtNo,
  60. foot,
  61. action: foot === 'right' ? '执行右脚程序' : '执行左脚程序',
  62. },
  63. }
  64. }
  65. function normalizeFoot(raw: unknown): FootSide | null {
  66. const text = String(raw ?? '').trim()
  67. if (!text) return null
  68. const value = text.toLowerCase()
  69. if (value === 'right' || value === 'r' || value === '1' || text.includes('右')) return 'right'
  70. if (value === 'left' || value === 'l' || value === '0' || text.includes('左')) return 'left'
  71. return null
  72. }
  73. export async function requestTakePhoto(payload: any) {
  74. const parsed = parseTakePhotoBody(payload)
  75. if (!parsed.ok) return { code: 1, msg: parsed.msg }
  76. if (shooting) return { code: 1, msg: '拍摄程序正在运行,请稍候' }
  77. const clientStore = client()
  78. if (!clientStore.isClient || !clientStore.ipc) {
  79. return { code: 1, msg: '拍照接口仅在客户端内可用' }
  80. }
  81. shooting = true
  82. const pending = waitForShoot(clientStore.ipc)
  83. try {
  84. const started = trigger
  85. ? await trigger(parsed.params)
  86. : await sendRunMcu(parsed.params)
  87. if (pending.settled()) {
  88. const result = await pending.promise
  89. return withData(parsed.params, result)
  90. }
  91. if (!started.started) {
  92. pending.cancel()
  93. return { code: started.code || 1, msg: started.msg || '未能开始拍摄' }
  94. }
  95. const result = await pending.promise
  96. return withData(parsed.params, result)
  97. } catch (error: any) {
  98. pending.cancel()
  99. return { code: 1, msg: error?.message || '拍照失败' }
  100. } finally {
  101. shooting = false
  102. }
  103. }
  104. function withData(params: TakePhotoParams, result: { code: number; msg: string }) {
  105. return {
  106. code: result.code,
  107. msg: result.msg,
  108. data: {
  109. goods_art_no: params.goods_art_no,
  110. foot: params.foot,
  111. action: params.action,
  112. },
  113. }
  114. }
  115. /** 拍摄页未打开时,仍下发与左右脚按钮相同的 run_mcu。 */
  116. async function sendRunMcu(params: TakePhotoParams): Promise<TriggerResult> {
  117. const socketStore = socket()
  118. await Promise.race([
  119. socketStore.connectSocket(),
  120. new Promise((_, reject) => {
  121. setTimeout(() => reject(new Error('设备服务未连接')), 8000)
  122. }),
  123. ])
  124. socketStore.sendMessage({
  125. type: 'run_mcu',
  126. data: {
  127. action: params.action,
  128. goods_art_no: params.goods_art_no,
  129. },
  130. })
  131. return { started: true, code: 0 }
  132. }
  133. function waitForShoot(ipc: IpcLike) {
  134. let settled = false
  135. let stepError = ''
  136. let stopped = false
  137. let resolveFn: (value: { code: number; msg: string }) => void = () => {}
  138. const promise = new Promise<{ code: number; msg: string }>((resolve) => {
  139. resolveFn = resolve
  140. })
  141. const channels = {
  142. runMcu: icpList.socket.message + '_run_mcu',
  143. mcu: icpList.socket.message + '_mcu',
  144. photoTake: icpList.socket.message + '_photo_take',
  145. imageProcess: icpList.socket.message + '_image_process',
  146. finish: icpList.socket.message + '_photo_take_finish',
  147. stop: icpList.socket.message + '_run_mcu_stop',
  148. }
  149. const handlers: Record<string, (...args: any[]) => void> = {}
  150. let timer: ReturnType<typeof setTimeout>
  151. let release = () => {}
  152. const done = (result: { code: number; msg: string }, reset = false) => {
  153. if (settled) return
  154. settled = true
  155. clearTimeout(timer)
  156. release()
  157. if (reset) onFail?.(result.msg)
  158. resolveFn(result)
  159. }
  160. const failIfLinkLost = (result: any) => {
  161. const msg = linkErrorMessage(result)
  162. if (!msg) return false
  163. done({ code: 1, msg }, true)
  164. return true
  165. }
  166. handlers[channels.runMcu] = (_event, result) => {
  167. if (failIfLinkLost(result)) return
  168. if (result && result.code !== 0) {
  169. done({ code: 1, msg: result.msg || '拍照失败' })
  170. }
  171. }
  172. handlers[channels.mcu] = (_event, result) => {
  173. if (failIfLinkLost(result)) return
  174. const msg = String(result?.msg || '')
  175. if (result?.code === 1 && MCU_ERRORS.some((item) => msg.includes(item))) {
  176. done({ code: 1, msg }, true)
  177. }
  178. }
  179. handlers[channels.photoTake] = (_event, result) => {
  180. if (failIfLinkLost(result)) return
  181. if (result?.code !== 0 && result?.msg) stepError = result.msg
  182. }
  183. handlers[channels.imageProcess] = (_event, result) => {
  184. failIfLinkLost(result)
  185. }
  186. handlers[channels.stop] = (_event, result) => {
  187. stopped = true
  188. stepError = result?.msg || '拍摄已终止'
  189. }
  190. handlers[channels.finish] = (_event, result) => {
  191. if (stopped || stepError) {
  192. done({ code: 1, msg: stepError || '拍摄失败' })
  193. return
  194. }
  195. if (result?.code === 0) {
  196. done({ code: 0, msg: result.msg || '拍摄完成' })
  197. return
  198. }
  199. done({ code: 1, msg: result?.msg || '拍照失败' }, true)
  200. }
  201. timer = setTimeout(() => {
  202. done({ code: 1, msg: '拍照超时' }, true)
  203. }, TAKE_PHOTO_TIMEOUT)
  204. release = () => {
  205. Object.entries(handlers).forEach(([channel, handler]) => keepOff(ipc, channel, handler))
  206. }
  207. ensureKeep(ipc)
  208. Object.entries(handlers).forEach(([channel, handler]) => keepOn(ipc, channel, handler))
  209. return {
  210. promise,
  211. settled: () => settled,
  212. cancel: () => {
  213. if (settled) return
  214. settled = true
  215. clearTimeout(timer)
  216. release()
  217. },
  218. }
  219. }
  220. function linkErrorMessage(result: any): string | null {
  221. const msg = String(result?.msg || '')
  222. if (!msg) return null
  223. if (LINK_ERRORS.some((item) => msg.includes(item))) return msg
  224. const status = result?.status ?? result?.device_status
  225. if (status === -1 && (msg.includes('相机') || msg.includes('MCU') || msg.includes('mcu'))) return msg
  226. return null
  227. }
  228. function keepOn(ipc: IpcLike, channel: string, handler: (...args: any[]) => void) {
  229. let set = keptListeners.get(channel)
  230. if (!set) {
  231. set = new Set()
  232. keptListeners.set(channel, set)
  233. }
  234. set.add(handler)
  235. ipc.on(channel, handler)
  236. }
  237. function keepOff(ipc: IpcLike, channel: string, handler: (...args: any[]) => void) {
  238. const set = keptListeners.get(channel)
  239. set?.delete(handler)
  240. if (set && set.size === 0) keptListeners.delete(channel)
  241. if (typeof ipc.removeListener === 'function') {
  242. ipc.removeListener(channel, handler)
  243. } else if (typeof ipc.off === 'function') {
  244. ipc.off(channel, handler)
  245. }
  246. }
  247. /** 拍摄页卸载时会 removeAllListeners,进行中的接口监听需要挂回去。 */
  248. function ensureKeep(ipc: IpcLike) {
  249. if (!ipc || ipc.__takePhotoOriginalRemoveAll || typeof ipc.removeAllListeners !== 'function') return
  250. const original = ipc.removeAllListeners.bind(ipc)
  251. ipc.__takePhotoOriginalRemoveAll = original
  252. ipc.removeAllListeners = (channel?: string) => {
  253. const result = original(channel)
  254. const channels = channel ? [channel] : Array.from(keptListeners.keys())
  255. channels.forEach((item) => {
  256. keptListeners.get(item)?.forEach((handler) => ipc.on(item, handler))
  257. })
  258. return result
  259. }
  260. }