// electron/utils/readConfig.js const path = require('path'); const fs = require('fs'); const Log = require('ee-core/log'); const { spawn } = require('child_process'); const { EventEmitter } = require('events'); const { liveShow, liveHide, setParams, capture, getParams,CMD,captureLive,closeOtherWindow,minimizeSmartShooter } = require('../api/camera'); const { dialog } = require('electron'); // 引入 electron 的 dialog 模块 const CoreWindow = require("ee-core/electron/window"); const { readConfigFile, updateConfigFile } = require('../utils/config'); const defaultConfig = require('./config.default'); // 进程状态管理 let cameraProcess = null; // 子进程引用 let isProcessRunning = false; // 进程运行状态 const processEmitter = new EventEmitter(); // 进程事件发射器 // Smart Shooter 5 / 6 与 digiCamControl 并存。 // 5 和 6 共用同一套 External API(54543 事件 / 54544 请求),不能同时开,按 controlType 只启动其中一个。 const CONTROLLERS = { digiCamControl: { exe: "CameraControl.exe", pathKey: "digiCamControlPath", label: "digiCamControl", }, SmartShooter: { exe: "SmartShooter5.exe", pathKey: "SmartShooterPath", label: "Smart Shooter 5", }, SmartShooter6: { exe: "SmartShooter6.exe", pathKey: "SmartShooter6Path", label: "Smart Shooter 6", }, } // 历史配置 controlType 仍是 SmartShooter,保持原行为 function normalizeControlType(type) { return CONTROLLERS[type] ? type : "SmartShooter" } function isSmartShooterType(type) { return type === "SmartShooter" || type === "SmartShooter6" } function sameDir(a, b) { if (!a || !b) return false const norm = (p) => path.normalize(String(p)).replace(/[\\/]+$/, "").toLowerCase() return norm(a) === norm(b) } // 只在仍使用出厂的 Smart Shooter 6 目录时回退,手动改过安装目录的保持原选择 function isDefaultSmartShooter6Dir(dir) { return sameDir(dir, defaultConfig.SmartShooter6Path) || sameDir(dir, defaultConfig.controlPath) } function findSmartShooter5Dir(config) { const candidates = [config.SmartShooterPath, defaultConfig.SmartShooterPath] const seen = new Set() for (const dir of candidates) { if (!dir) continue const key = path.normalize(String(dir)).replace(/[\\/]+$/, "").toLowerCase() if (seen.has(key)) continue seen.add(key) if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter.exe))) return dir } 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) if (ss5Dir) { Log.info(`默认目录未找到 SmartShooter6.exe,改用 Smart Shooter 5: ${ss5Dir}`) return { type: "SmartShooter", ...CONTROLLERS.SmartShooter, dir: ss5Dir, } } } return { type, ...spec, dir, } } function getExePath () { const controller = getController() const exePath = controller.dir ? path.join(controller.dir, controller.exe) : "" console.log('ex============ePath'); console.log(exePath); return exePath } // 用户手动选安装目录时,按目录里实际存在的 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" return "" } let ensureControllerPromise = null async function checkCameraControlCmdExists() { if (!ensureControllerPromise) { ensureControllerPromise = ensureCameraController().finally(() => { ensureControllerPromise = null }) } return ensureControllerPromise } async function ensureCameraController() { try { // 拼接 CameraControlCmd.exe 的完整路径 let exePath = getExePath() // 检查文件是否存在 const exists = await fs.promises.access(exePath, fs.constants.F_OK) .then(() => true) .catch(() => false); if (!exists) { // 弹出文件夹选择对话框 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]; const detected = detectController(selectedPath); if (!detected) { console.error('所选目录不是已知的相机控制软件'); return { status:-1, msg:"所选目录中未找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe", } } saveControllerChoice(detected, selectedPath) } else { console.error('用户未选择文件夹'); return { status:-1, msg:"无法找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe", } } } const res = await openCameraControlCmd(); // Smart Shooter 5 / 6 启动后最小化窗口;digiCamControl 走自己的最小化命令 if(isSmartShooterType(getController().type)){ // 等待软件启动完成 setTimeout(async () => { try { await minimizeSmartShooter(); } catch (error) { console.log('最小化SmartShooter失败:', error); } }, 3000); // 等待3秒让软件完全启动 } return res; } catch (error) { Log.error('检查 第三方相机控制器 是否存在时出错:', error); throw error; } } 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) } let child try { child = spawn(exePath, [], { cwd: path.dirname(exePath), stdio: 'ignore', }) } catch (error) { Log.error('无法启动第三方相机控制器:', error) finish(reject, error) return } 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'); }); child.on('error', (err) => { console.log('[CAMERA-DEBUG] error 事件触发:', err.message); isProcessRunning = false; cameraProcess = null; Log.error('相机控制软件进程错误:', err); finish(reject, err) }); // 图形界面程序通常没有 stdout,进程拉起即视为启动成功 child.once('spawn', () => finish(resolve, true)) if (child.pid) finish(resolve, true) }).catch((error) => { Log.error('无法找到或运行 第三方相机控制器:', error); reject(error) }) }) } async function closeCameraControlTips() { try { await closeOtherWindow() }catch (e) { console.log(e) } } module.exports = { checkCameraControlCmdExists, closeCameraControlTips, minimizeSmartShooter, CONTROLLERS, isSmartShooterType, getController, // 进程状态管理 isProcessRunning: () => isProcessRunning, onProcessClosed: (callback) => processEmitter.on('camera-process-closed', callback), removeProcessClosedListener: (callback) => processEmitter.removeListener('camera-process-closed', callback), };