Selaa lähdekoodia

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

- 合并 digiCamControl 与 SmartShooter 的启动流程为统一逻辑
- getController 增加按目录实际 exe 自动识别控制器类型
- checkCameraControlCmdExists 增加 Promise 缓存防止并发重复启动
- openCameraControlCmd 改用 spawn 事件判断启动成功替代 stdout 监听
- 新增 updateConfigFile 支持批量写入避免配置项互相覆盖
- 提取 saveControllerChoice 统一保存控制器选择结果
panqiuyao 6 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';
 'use strict';
 const { Controller } = require('ee-core');
 const { Controller } = require('ee-core');
 const Log = require('ee-core/log');
 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 {
 const {
   checkCamera,
   checkCamera,
   liveShow: liveShowApi, liveHide: liveHideApi, setParams, capture, getParams,CMD,captureLive,closeOtherWindow } = require('../api/camera');
   liveShow: liveShowApi, liveHide: liveHideApi, setParams, capture, getParams,CMD,captureLive,closeOtherWindow } = require('../api/camera');
@@ -40,22 +40,28 @@ class CameraController extends Controller {
         isSoftwareStarted = false;
         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 CMD('All_Minimize');
           await closeCameraControlTips();
           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=>{
         let res = await getParams('iso').catch(e=>{
           console.log('getParams failed:', e);
           console.log('getParams failed:', e);
           isOPen = false;
           isOPen = false;
@@ -73,59 +79,42 @@ class CameraController extends Controller {
           status:2,
           status:2,
           msg:res,
           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) {
     } catch (error) {
 
 
-      const controlType = readConfigFile().controlType
+      const controlType = getController().type
       let msg = '请安装 Smart Shooter 5 或 Smart Shooter 6'
       let msg = '请安装 Smart Shooter 5 或 Smart Shooter 6'
       if(controlType === 'SmartShooter') msg = '请安装 Smart Shooter 5'
       if(controlType === 'SmartShooter') msg = '请安装 Smart Shooter 5'
       if(controlType === 'SmartShooter6') msg = '请安装 Smart Shooter 6 或 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 CoreWindow = require("ee-core/electron/window");
 
 
 
 
-const { readConfigFile, writeConfigFile } = require('../utils/config');
+const { readConfigFile, updateConfigFile } = require('../utils/config');
 const defaultConfig = require('./config.default');
 const defaultConfig = require('./config.default');
 
 
 // 进程状态管理
 // 进程状态管理
@@ -72,12 +72,67 @@ function findSmartShooter5Dir(config) {
   return ""
   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() {
 function getController() {
   const config = readConfigFile() || {}
   const config = readConfigFile() || {}
   const type = normalizeControlType(config.controlType)
   const type = normalizeControlType(config.controlType)
   const spec = CONTROLLERS[type]
   const spec = CONTROLLERS[type]
   const dir = config.controlPath || config[spec.pathKey] || ""
   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。只影响本次启动,不回写配置。
   // 默认 Smart Shooter 6 目录下没有程序时,再找 Smart Shooter 5。只影响本次启动,不回写配置。
   if (type === "SmartShooter6" && isDefaultSmartShooter6Dir(dir) && !fs.existsSync(path.join(dir, spec.exe))) {
   if (type === "SmartShooter6" && isDefaultSmartShooter6Dir(dir) && !fs.existsSync(path.join(dir, spec.exe))) {
     const ss5Dir = findSmartShooter5Dir(config)
     const ss5Dir = findSmartShooter5Dir(config)
@@ -109,6 +164,7 @@ function getExePath () {
 
 
 // 用户手动选安装目录时,按目录里实际存在的 exe 识别,5 和 6 互不覆盖
 // 用户手动选安装目录时,按目录里实际存在的 exe 识别,5 和 6 互不覆盖
 function detectController(dir) {
 function detectController(dir) {
+  if (!dir) return ""
   if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter6.exe))) return "SmartShooter6"
   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.SmartShooter.exe))) return "SmartShooter"
   if (fs.existsSync(path.join(dir, CONTROLLERS.digiCamControl.exe))) return "digiCamControl"
   if (fs.existsSync(path.join(dir, CONTROLLERS.digiCamControl.exe))) return "digiCamControl"
@@ -118,7 +174,18 @@ function detectController(dir) {
 
 
 
 
 
 
+let ensureControllerPromise = null
+
 async function checkCameraControlCmdExists() {
 async function checkCameraControlCmdExists() {
+  if (!ensureControllerPromise) {
+    ensureControllerPromise = ensureCameraController().finally(() => {
+      ensureControllerPromise = null
+    })
+  }
+  return ensureControllerPromise
+}
+
+async function ensureCameraController() {
   try {
   try {
 
 
     // 拼接 CameraControlCmd.exe 的完整路径
     // 拼接 CameraControlCmd.exe 的完整路径
@@ -131,10 +198,14 @@ async function checkCameraControlCmdExists() {
 
 
     if (!exists) {
     if (!exists) {
       // 弹出文件夹选择对话框
       // 弹出文件夹选择对话框
-      const { canceled, filePaths } = await dialog.showOpenDialog({
+      const dialogOptions = {
         title: '选择 相机控制安装软件 文件夹',
         title: '选择 相机控制安装软件 文件夹',
         properties: ['openDirectory']
         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) {
       if (!canceled && filePaths.length > 0) {
         const selectedPath = filePaths[0];
         const selectedPath = filePaths[0];
@@ -147,9 +218,7 @@ async function checkCameraControlCmdExists() {
             msg:"所选目录中未找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe",
             msg:"所选目录中未找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe",
           }
           }
         }
         }
-        writeConfigFile("controlType", detected)
-        writeConfigFile("controlPath", selectedPath)
-        writeConfigFile(CONTROLLERS[detected].pathKey, selectedPath)
+        saveControllerChoice(detected, selectedPath)
       } else {
       } else {
         console.error('用户未选择文件夹');
         console.error('用户未选择文件夹');
         return {
         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 {
       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);
       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 {
   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');
     fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
-    console.log(`成功写入配置项: ${key}`);
+    console.log(`成功写入配置项: ${Object.keys(partial).join(', ')}`);
   } catch (error) {
   } catch (error) {
     console.error('写入配置文件出错:', error);
     console.error('写入配置文件出错:', error);
   }
   }
 }
 }
 
 
+/**
+ * 写入配置文件
+ * @param {string} key - 要写入的配置项键名
+ * @param {*} value - 要写入的配置项值
+ */
+function writeConfigFile(key, value) {
+  updateConfigFile({ [key]: value })
+}
+
 module.exports = {
 module.exports = {
   readConfigFile,
   readConfigFile,
-  writeConfigFile
+  writeConfigFile,
+  updateConfigFile
 };
 };