Selaa lähdekoodia

refactor(camera): 统一相机控制器启动与检测逻辑

- 合并 digiCamControl 与 SmartShooter 的启动流程为统一逻辑
- getController 增加按目录实际 exe 自动识别控制器类型
- checkCameraControlCmdExists 增加 Promise 缓存防止并发重复启动
- openCameraControlCmd 改用 spawn 事件判断启动成功替代 stdout 监听
- 新增 updateConfigFile 支持批量写入避免配置项互相覆盖
- 提取 saveControllerChoice 统一保存控制器选择结果
panqiuyao 3 tuntia sitten
vanhempi
commit
70840f8242
3 muutettua tiedostoa jossa 191 lisäystä ja 132 poistoa
  1. 45 56
      electron/controller/camera.js
  2. 118 48
      electron/utils/camera.js
  3. 28 28
      electron/utils/config.js

+ 45 - 56
electron/controller/camera.js

@@ -1,7 +1,7 @@
 'use strict';
 const { Controller } = require('ee-core');
 const Log = require('ee-core/log');
-const { checkCameraControlCmdExists, closeCameraControlTips, isProcessRunning, onProcessClosed, removeProcessClosedListener } = require('../utils/camera');
+const { checkCameraControlCmdExists, closeCameraControlTips, isProcessRunning, onProcessClosed, removeProcessClosedListener, getController } = require('../utils/camera');
 const {
   checkCamera,
   liveShow: liveShowApi, liveHide: liveHideApi, setParams, capture, getParams,CMD,captureLive,closeOtherWindow } = require('../api/camera');
@@ -40,22 +40,28 @@ class CameraController extends Controller {
         isSoftwareStarted = false;
       }
 
-      if(readConfigFile().controlType === 'digiCamControl'){
-        console.log('========1==========');
-
-        // 主动检测:如果软件未启动或进程已关闭,先启动软件
-        if (!isSoftwareStarted || !isProcessRunning()) {
-          console.log('[CAMERA-DEBUG] 准备启动 digiCamControl - isSoftwareStarted:', isSoftwareStarted, 'isProcessRunning:', isProcessRunning());
-          console.log('启动 digiCamControl 软件...');
-          await checkCameraControlCmdExists();
+      if (!isSoftwareStarted || !isProcessRunning()) {
+        console.log('[CAMERA-DEBUG] 准备启动相机控制软件 - isSoftwareStarted:', isSoftwareStarted, 'isProcessRunning:', isProcessRunning());
+        const ready = await checkCameraControlCmdExists();
+        if (ready && ready.status === -1) {
+          return ready;
+        }
+        isSoftwareStarted = true;
+        const startedType = getController().type;
+        console.log('[CAMERA-DEBUG] 相机控制软件已启动,类型:', startedType);
+        if (startedType === 'digiCamControl') {
           await CMD('All_Minimize');
           await closeCameraControlTips();
-          isSoftwareStarted = true;
-          console.log('digiCamControl 软件启动完成,等待初始化...');
-          await new Promise(resolve => setTimeout(resolve, 5000)); // 等待软件初始化
+          await new Promise(resolve => setTimeout(resolve, 5000));
+        } else {
+          Log.info('SmartShooter 软件启动完成,等待初始化...');
+          await new Promise(resolve => setTimeout(resolve, 10000));
         }
+      }
 
-        // 获取相机参数
+      // 启动完成后再读类型。选目录时可能把 digiCamControl 改成 SmartShooter6
+      if (getController().type === 'digiCamControl') {
+        console.log('========1==========');
         let res = await getParams('iso').catch(e=>{
           console.log('getParams failed:', e);
           isOPen = false;
@@ -73,59 +79,42 @@ class CameraController extends Controller {
           status:2,
           msg:res,
         }
-      }else{
-        // SmartShooter 逻辑
-        // 主动检测:如果软件未启动或进程已关闭,先启动软件
-        console.log('[CAMERA-DEBUG] SmartShooter 分支 - isSoftwareStarted:', isSoftwareStarted, 'isProcessRunning():', isProcessRunning());
-        if (!isSoftwareStarted || !isProcessRunning()) {
-          console.log('[CAMERA-DEBUG] 准备启动 SmartShooter - isSoftwareStarted:', isSoftwareStarted, 'isProcessRunning:', isProcessRunning());
-          Log.info('启动 SmartShooter 软件...');
-          Log.info('isSoftwareStarted...'+isSoftwareStarted);
-          Log.info('isProcessRunning...'+isProcessRunning);
-          await checkCameraControlCmdExists();
-          isSoftwareStarted = true;
-          console.log('[CAMERA-DEBUG] checkCameraControlCmdExists() 完成,isSoftwareStarted:', isSoftwareStarted, 'isProcessRunning():', isProcessRunning());
-          Log.info('SmartShooter 软件启动完成,等待初始化...');
-          await new Promise(resolve => setTimeout(resolve, 10000)); // 等待软件初始化
-        }
+      }
 
-        // 检查相机状态
-        let res = await checkCamera(isMultiCameraMode).catch(e=>{
-          Log.info('checkCamera:'+e);
-          isOPen = false;
-          return { device_status: -1 }; // 返回错误状态,让后续逻辑处理
-        })
+      console.log('[CAMERA-DEBUG] SmartShooter 分支 - isSoftwareStarted:', isSoftwareStarted, 'isProcessRunning():', isProcessRunning());
+      let res = await checkCamera(isMultiCameraMode).catch(e=>{
+        Log.info('checkCamera:'+e);
+        isOPen = false;
+        return { device_status: -1 };
+      })
 
-        // 如果相机未连接
-        if (res?.device_status === -1) {
-          return {
-            status: -1,
-            msg: "相机硬件未连接,请检查相机连接。",
-          };
-        }
+      if (res?.device_status === -1) {
+        return {
+          status: -1,
+          msg: "相机硬件未连接,请检查相机连接。",
+        };
+      }
 
-        if(res?.device_status  === 2){
-          isOPen = true;
-          // 缓存相机列表数据(已包含各相机的 ISO 配置)
-          if (res.CameraLists) {
-            cachedCameraList = res.CameraLists;
-          }
-          return  {
-            ...res,
-            status:2
-          };
+      if(res?.device_status  === 2){
+        isOPen = true;
+        if (res.CameraLists) {
+          cachedCameraList = res.CameraLists;
         }
+        return  {
+          ...res,
+          status:2
+        };
+      }
 
-        return {
-          status:-1,
-          msg:"相机未连接,请链接相机。",
-        }
+      return {
+        status:-1,
+        msg:"相机未连接,请链接相机。",
       }
 
 
     } catch (error) {
 
-      const controlType = readConfigFile().controlType
+      const controlType = getController().type
       let msg = '请安装 Smart Shooter 5 或 Smart Shooter 6'
       if(controlType === 'SmartShooter') msg = '请安装 Smart Shooter 5'
       if(controlType === 'SmartShooter6') msg = '请安装 Smart Shooter 6 或 Smart Shooter 5'

+ 118 - 48
electron/utils/camera.js

@@ -11,7 +11,7 @@ const { dialog } = require('electron'); // 引入 electron 的 dialog 模块
 const CoreWindow = require("ee-core/electron/window");
 
 
-const { readConfigFile, writeConfigFile } = require('../utils/config');
+const { readConfigFile, updateConfigFile } = require('../utils/config');
 const defaultConfig = require('./config.default');
 
 // 进程状态管理
@@ -72,12 +72,67 @@ function findSmartShooter5Dir(config) {
   return ""
 }
 
+function saveControllerChoice(type, dir) {
+  const spec = CONTROLLERS[type]
+  if (!spec || !dir) return
+  updateConfigFile({
+    controlType: type,
+    controlPath: dir,
+    [spec.pathKey]: dir,
+  })
+}
+
+function controllerChoiceMatches(config, type, dir) {
+  const spec = CONTROLLERS[type]
+  if (!spec) return false
+  return config.controlType === type
+    && sameDir(config.controlPath, dir)
+    && sameDir(config[spec.pathKey], dir)
+}
+
+function uniqueDirs(dirs) {
+  const result = []
+  for (const dir of dirs) {
+    if (!dir) continue
+    if (result.some((item) => sameDir(item, dir))) continue
+    result.push(dir)
+  }
+  return result
+}
+
+function rememberController(config, detected, dir) {
+  if (!controllerChoiceMatches(config, detected, dir)) {
+    Log.info(`按安装目录识别为 ${CONTROLLERS[detected].label}: ${dir}`)
+    saveControllerChoice(detected, dir)
+  }
+  return {
+    type: detected,
+    ...CONTROLLERS[detected],
+    dir,
+  }
+}
+
 function getController() {
   const config = readConfigFile() || {}
   const type = normalizeControlType(config.controlType)
   const spec = CONTROLLERS[type]
   const dir = config.controlPath || config[spec.pathKey] || ""
 
+  // 上次选中的目录里有 exe 时,以那个 exe 为准。
+  // 避免 controlPath 已是 Smart Shooter 6,controlType 仍是 digiCamControl。
+  const selected = detectController(config.controlPath)
+  if (selected) return rememberController(config, selected, config.controlPath)
+
+  // controlPath 无效时,再找该类型自己的目录和出厂目录,避免坏路径挡住已经装好的程序。
+  const fallbackDirs = uniqueDirs([
+    config[spec.pathKey],
+    defaultConfig[spec.pathKey],
+    type === "SmartShooter6" ? defaultConfig.controlPath : "",
+  ])
+  for (const candidate of fallbackDirs) {
+    if (detectController(candidate) === type) return rememberController(config, type, candidate)
+  }
+
   // 默认 Smart Shooter 6 目录下没有程序时,再找 Smart Shooter 5。只影响本次启动,不回写配置。
   if (type === "SmartShooter6" && isDefaultSmartShooter6Dir(dir) && !fs.existsSync(path.join(dir, spec.exe))) {
     const ss5Dir = findSmartShooter5Dir(config)
@@ -109,6 +164,7 @@ function getExePath () {
 
 // 用户手动选安装目录时,按目录里实际存在的 exe 识别,5 和 6 互不覆盖
 function detectController(dir) {
+  if (!dir) return ""
   if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter6.exe))) return "SmartShooter6"
   if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter.exe))) return "SmartShooter"
   if (fs.existsSync(path.join(dir, CONTROLLERS.digiCamControl.exe))) return "digiCamControl"
@@ -118,7 +174,18 @@ function detectController(dir) {
 
 
 
+let ensureControllerPromise = null
+
 async function checkCameraControlCmdExists() {
+  if (!ensureControllerPromise) {
+    ensureControllerPromise = ensureCameraController().finally(() => {
+      ensureControllerPromise = null
+    })
+  }
+  return ensureControllerPromise
+}
+
+async function ensureCameraController() {
   try {
 
     // 拼接 CameraControlCmd.exe 的完整路径
@@ -131,10 +198,14 @@ async function checkCameraControlCmdExists() {
 
     if (!exists) {
       // 弹出文件夹选择对话框
-      const { canceled, filePaths } = await dialog.showOpenDialog({
+      const dialogOptions = {
         title: '选择 相机控制安装软件 文件夹',
         properties: ['openDirectory']
-      });
+      }
+      const mainWindow = CoreWindow.getMainWindow()
+      const { canceled, filePaths } = mainWindow && !mainWindow.isDestroyed()
+        ? await dialog.showOpenDialog(mainWindow, dialogOptions)
+        : await dialog.showOpenDialog(dialogOptions);
 
       if (!canceled && filePaths.length > 0) {
         const selectedPath = filePaths[0];
@@ -147,9 +218,7 @@ async function checkCameraControlCmdExists() {
             msg:"所选目录中未找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe",
           }
         }
-        writeConfigFile("controlType", detected)
-        writeConfigFile("controlPath", selectedPath)
-        writeConfigFile(CONTROLLERS[detected].pathKey, selectedPath)
+        saveControllerChoice(detected, selectedPath)
       } else {
         console.error('用户未选择文件夹');
         return {
@@ -180,55 +249,56 @@ async function checkCameraControlCmdExists() {
 }
 
 
-async function openCameraControlCmd(digiCamControlPath) {
-  return  new Promise(async (resolve, reject) => {
-    try {
-      // 获取 digiCamControl 文件夹路径
-
-      // 拼接 CameraControlCmd.exe 的完整路径
-      let exePath = getExePath()
+function openCameraControlCmd() {
+  return new Promise((resolve, reject) => {
+    const exePath = getExePath()
+    fs.promises.access(exePath, fs.constants.F_OK).then(() => {
+      let settled = false
+      const finish = (fn, value) => {
+        if (settled) return
+        settled = true
+        fn(value)
+      }
 
-      // 检查文件是否存在
-      await fs.promises.access(exePath, fs.constants.F_OK);
+      let child
       try {
+        child = spawn(exePath, [], {
+          cwd: path.dirname(exePath),
+          stdio: 'ignore',
+        })
+      } catch (error) {
+        Log.error('无法启动第三方相机控制器:', error)
+        finish(reject, error)
+        return
+      }
 
-        // 保存进程引用并标记为运行中
-        cameraProcess = spawn(exePath);
-        isProcessRunning = true;
-        console.log('[CAMERA-DEBUG] spawn PID:', cameraProcess.pid, '时间:', new Date().toISOString());
-
-        cameraProcess.stdout.on('data', (data) => {
-          console.log('[CAMERA-DEBUG] stdout 收到数据:', data.toString().substring(0, 100));
-          resolve(true)
-        });
-
-        // 监听进程关闭事件
-        cameraProcess.on('close', (code) => {
-          console.log('[CAMERA-DEBUG] close 事件触发! PID:', cameraProcess?.pid, '退出码:', code, '时间:', new Date().toISOString());
-          isProcessRunning = false;
-          cameraProcess = null;
-          Log.info(`相机控制软件已关闭,退出码: ${code}`);
-          processEmitter.emit('camera-process-closed');
-        });
-
-        cameraProcess.on('error', (err) => {
-          console.log('[CAMERA-DEBUG] error 事件触发:', err.message);
-          isProcessRunning = false;
-          cameraProcess = null;
-          Log.error('相机控制软件进程错误:', err);
-        });
-
+      cameraProcess = child
+      isProcessRunning = true
+      console.log('[CAMERA-DEBUG] spawn PID:', child.pid, 'cwd:', path.dirname(exePath), '时间:', new Date().toISOString());
 
+      child.on('close', (code) => {
+        console.log('[CAMERA-DEBUG] close 事件触发! PID:', cameraProcess?.pid, '退出码:', code, '时间:', new Date().toISOString());
+        isProcessRunning = false;
+        cameraProcess = null;
+        Log.info(`相机控制软件已关闭,退出码: ${code}`);
+        processEmitter.emit('camera-process-closed');
+      });
 
-      } catch (error) {
-        Log.error('error 第三方相机控制器:', error);
-        throw error;
-      }
+      child.on('error', (err) => {
+        console.log('[CAMERA-DEBUG] error 事件触发:', err.message);
+        isProcessRunning = false;
+        cameraProcess = null;
+        Log.error('相机控制软件进程错误:', err);
+        finish(reject, err)
+      });
 
-    } catch (error) {
+      // 图形界面程序通常没有 stdout,进程拉起即视为启动成功
+      child.once('spawn', () => finish(resolve, true))
+      if (child.pid) finish(resolve, true)
+    }).catch((error) => {
       Log.error('无法找到或运行 第三方相机控制器:', error);
-      throw error;
-    }
+      reject(error)
+    })
   })
 }
 

+ 28 - 28
electron/utils/config.js

@@ -59,44 +59,44 @@ function readConfigFile() {
   }
 }
 
+function loadConfigFile() {
+  if (!fs.existsSync(configPath)) {
+    return { ...defaultConfig }
+  }
+  const data = fs.readFileSync(configPath, 'utf8');
+  return {
+    ...defaultConfig,
+    ...JSON.parse(data),
+  }
+}
+
 /**
- * 写入配置文件
- * @param {string} key - 要写入的配置项键名
- * @param {*} value - 要写入的配置项值
+ * 一次写入多项配置,避免 controlType 和安装目录被分成几次写入后互相覆盖
  */
-function writeConfigFile(key, value) {
-
+function updateConfigFile(partial) {
   try {
-    let config = {};
-
-    // 如果配置文件存在,则读取现有内容
-    if (fs.existsSync(configPath)) {
-      const data = fs.readFileSync(configPath, 'utf8');
-      config = {
-        ...defaultConfig,
-        ...JSON.parse(data),
-      };
-    }else {
-      console.log('配置文件不存在');
-      // 创建空白JSON文件
-      fs.writeFileSync(configPath, '{}', 'utf8');
-      config = {
-        ...defaultConfig,
-      }
+    const config = {
+      ...loadConfigFile(),
+      ...partial,
     }
-
-    // 更新配置项
-    config[key] = value;
-
-    // 将更新后的配置写回文件
     fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
-    console.log(`成功写入配置项: ${key}`);
+    console.log(`成功写入配置项: ${Object.keys(partial).join(', ')}`);
   } catch (error) {
     console.error('写入配置文件出错:', error);
   }
 }
 
+/**
+ * 写入配置文件
+ * @param {string} key - 要写入的配置项键名
+ * @param {*} value - 要写入的配置项值
+ */
+function writeConfigFile(key, value) {
+  updateConfigFile({ [key]: value })
+}
+
 module.exports = {
   readConfigFile,
-  writeConfigFile
+  writeConfigFile,
+  updateConfigFile
 };